[FFL-2837] Add core provider and offline rules evaluation - #1456
Conversation
|
✅ All CI checks and tests passed. 🎉 All green!🧪 All tests passed 🔗 Commit SHA: 0537205 | Docs | View more details | Give us feedback! |
There was a problem hiding this comment.
Copilot review overview
🔵 Needs a closer look
The cross-package evaluator bridge, native tracking path, packaging, and backward compatibility warrant final human validation.
Review effort: Balanced
Findings: None
What changed in this PR
Adds shared JavaScript rules-based evaluation while preserving the offline provider’s native tracking and compatibility behavior.
Changes:
- Adds
DatadogCoreProviderand opt-in rules-based parsing. - Bridges offline evaluation through named native
FlagsClientinstances. - Adds packaging, documentation, compatibility handling, and broad tests.
| File | Description |
|---|---|
LICENSE-3rdparty.csv |
Records new production dependencies. |
yarn.lock |
Locks flagging-core and transitive dependencies. |
packages/react-native-openfeature/package.json |
Adds flagging-core and subpath packaging. |
packages/react-native-openfeature/rules-based/package.json |
Defines the rules-based entry point. |
packages/react-native-openfeature/release-content.txt |
Updates packaged-file expectations. |
packages/react-native-openfeature/README.md |
Documents core and rules-based evaluation. |
packages/react-native-openfeature/src/index.ts |
Exports the configuration type. |
packages/react-native-openfeature/src/rules-based.ts |
Exposes parser and core provider. |
packages/react-native-openfeature/src/datadogCoreProvider.ts |
Implements shared JavaScript evaluation. |
packages/react-native-openfeature/src/offlineEvaluation.ts |
Adapts evaluation to native tracking. |
packages/react-native-openfeature/src/offlineProvider.ts |
Installs the evaluator with legacy fallback. |
packages/react-native-openfeature/src/__tests__/__utils__/coreConfiguration.ts |
Adds reusable configuration fixtures. |
packages/react-native-openfeature/src/__tests__/datadogCoreProvider.test.ts |
Tests core-provider behavior. |
packages/react-native-openfeature/src/__tests__/datadogCoreProvider.integration.test.ts |
Tests OpenFeature integration. |
packages/react-native-openfeature/src/__tests__/entrypoints.test.ts |
Verifies parser entry-point isolation. |
packages/react-native-openfeature/src/__tests__/offlineProvider.delegation.test.ts |
Tests delegation and native tracking. |
packages/react-native-openfeature/src/__tests__/offlineProvider.test.ts |
Extends legacy compatibility tests. |
packages/core/src/flags/types.ts |
Adds delegated evaluation error codes. |
packages/core/src/flags/internal.ts |
Adds split serial metadata. |
packages/core/src/flags/FlagsClient.ts |
Adds the offline evaluator bridge. |
packages/core/src/flags/configuration/precomputed.ts |
Clarifies serial-ID handling. |
packages/core/src/flags/__tests__/FlagsClient.test.ts |
Tests bridge state and tracking. |
packages/core/android/src/main/kotlin/com/datadog/reactnative/DdFlagsImplementation.kt |
Omits null native attributes. |
packages/core/android/src/test/kotlin/com/datadog/reactnative/DdFlagsImplementationTest.kt |
Tests Android context conversion. |
packages/core/ios/Tests/RUMMocks.swift |
Adds explicit Foundation import. |
packages/core/ios/Tests/MockRUMMonitor.swift |
Adds explicit Foundation import. |
packages/core/ios/Tests/MockDatadogCore.swift |
Adds explicit Foundation import. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
33b2eba to
fa03a48
Compare
|
Looks good to me! 👍 I have left a few notes, and a small change request :) |
Route the rules-based compatibility warning through InternalLog at WARN verbosity instead of console.warn, so it respects the SDK's configured verbosity. InternalLog has been exported by the core package since 1.0.0. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Rename the base class in datadogCoreProvider.ts to DatadogCoreEvaluationProvider so it no longer shares a name with the public DatadogCoreProvider exported from the rules-based entry point. The public export is unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
btthomas
left a comment
There was a problem hiding this comment.
Nicely done.
Other than the missing evaluation/exposure logging, this looks pretty good.
I don't know how much we should be worried about customers with multiple (datadog) Providers that might share clientName.
…nchronously Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…E_ERROR Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
domalessi
left a comment
There was a problem hiding this comment.
Left some editorial suggestions to tighten up the copy, but nothing blocking!
| provider's context normalization, `clientName`, and native exposure/RUM tracking. It **never fetches | ||
| configuration from the network** — you supply it with `setConfiguration`. | ||
|
|
||
| Update `@datadog/mobile-react-native` and this package together to use the native tracking/evaluator |
There was a problem hiding this comment.
Specify the minimum versions of @datadog/mobile-react-native and @datadog/mobile-react-native-openfeature required for rules-based evaluation.
| If you fetch a flag configuration yourself (cached on disk, delivered via your own service, | ||
| or bundled with the app), use `DatadogOfflineOpenFeatureProvider` instead of | ||
| `DatadogOpenFeatureProvider`. It delegates **precomputed and rules-based evaluation** to | ||
| `DatadogCoreProvider` using the pinned `@datadog/flagging-core` evaluator, but keeps the offline | ||
| provider's context normalization, `clientName`, and native exposure/RUM tracking. It **never fetches | ||
| configuration from the network** — you supply it with `setConfiguration`. |
There was a problem hiding this comment.
| If you fetch a flag configuration yourself (cached on disk, delivered via your own service, | |
| or bundled with the app), use `DatadogOfflineOpenFeatureProvider` instead of | |
| `DatadogOpenFeatureProvider`. It delegates **precomputed and rules-based evaluation** to | |
| `DatadogCoreProvider` using the pinned `@datadog/flagging-core` evaluator, but keeps the offline | |
| provider's context normalization, `clientName`, and native exposure/RUM tracking. It **never fetches | |
| configuration from the network** — you supply it with `setConfiguration`. | |
| Use `DatadogOfflineOpenFeatureProvider` to evaluate manually supplied precomputed or rules-based configurations with native exposure or RUM tracking. The provider does not fetch configuration; supply it with `setConfiguration`. For evaluation without tracking, use `DatadogCoreProvider`. |
| Use `DatadogCoreProvider` to evaluate precomputed or rules-based configurations entirely in | ||
| JavaScript with `@datadog/flagging-core`. This is a port of the browser `DatadogCoreProvider`; | ||
| it does not use the native `FlagsClient`, fetch configuration, or send exposure or RUM events. | ||
| Your application owns configuration delivery, storage, and updates. |
There was a problem hiding this comment.
| Use `DatadogCoreProvider` to evaluate precomputed or rules-based configurations entirely in | |
| JavaScript with `@datadog/flagging-core`. This is a port of the browser `DatadogCoreProvider`; | |
| it does not use the native `FlagsClient`, fetch configuration, or send exposure or RUM events. | |
| Your application owns configuration delivery, storage, and updates. | |
| Use `DatadogCoreProvider` to evaluate manually supplied precomputed or rules-based configurations in JavaScript. The provider does not fetch configuration or send exposure or RUM events. Your application manages configuration delivery, storage, and updates. |
| You can also supply a parsed `FlagsConfiguration` directly; the type is exported by both entry points. | ||
| `getConfiguration()` returns the currently supplied configuration, or `undefined` before one is set. |
There was a problem hiding this comment.
| You can also supply a parsed `FlagsConfiguration` directly; the type is exported by both entry points. | |
| `getConfiguration()` returns the currently supplied configuration, or `undefined` before one is set. | |
| You can also supply a parsed `FlagsConfiguration` directly. Both entry points export this type. `getConfiguration()` returns the supplied configuration, or `undefined` if no configuration is set. |
| - Matching precomputed data takes precedence over rules. If the context does not match, the | ||
| evaluator uses rules when available; otherwise evaluation returns your coded default with | ||
| `INVALID_CONTEXT` (or `PARSE_ERROR` if the fallback capability could not be parsed). |
There was a problem hiding this comment.
| - Matching precomputed data takes precedence over rules. If the context does not match, the | |
| evaluator uses rules when available; otherwise evaluation returns your coded default with | |
| `INVALID_CONTEXT` (or `PARSE_ERROR` if the fallback capability could not be parsed). | |
| - Matching precomputed data takes precedence over rules. If the context does not match, the evaluator uses rules when available. Otherwise, evaluation returns your coded default with `INVALID_CONTEXT`, or `PARSE_ERROR` if the rules could not be parsed. |
| - Load configuration **before** registration. Missing configuration rejects initialization with | ||
| `PROVIDER_NOT_READY`; unusable configuration produces `PARSE_ERROR`. If registration fails, | ||
| wait for that initialization to settle before loading a valid configuration to recover. | ||
| - Replacing a usable configuration emits `ConfigurationChanged`. An unusable replacement emits | ||
| `Error`; loading a usable configuration after an error emits `Ready` then `ConfigurationChanged`. |
There was a problem hiding this comment.
| - Load configuration **before** registration. Missing configuration rejects initialization with | |
| `PROVIDER_NOT_READY`; unusable configuration produces `PARSE_ERROR`. If registration fails, | |
| wait for that initialization to settle before loading a valid configuration to recover. | |
| - Replacing a usable configuration emits `ConfigurationChanged`. An unusable replacement emits | |
| `Error`; loading a usable configuration after an error emits `Ready` then `ConfigurationChanged`. | |
| - Load configuration before registering the provider. Missing configuration causes initialization to fail with `PROVIDER_NOT_READY`. Unusable configuration produces `PARSE_ERROR`. If initialization fails, wait for it to finish before loading a valid configuration. | |
| - Replacing a usable configuration emits `ConfigurationChanged`. An unusable replacement emits `Error`. Loading a usable configuration after an error emits `Ready`, followed by `ConfigurationChanged`. |
| Use `coreConfigurationFromString` from the `/rules-based` entry point for rules or combined | ||
| precomputed/rules payloads. The legacy `configurationFromString` helper remains precomputed-only. Rules are evaluated locally for the | ||
| current context, so you can change users or targeting attributes without fetching a new configuration: |
There was a problem hiding this comment.
| Use `coreConfigurationFromString` from the `/rules-based` entry point for rules or combined | |
| precomputed/rules payloads. The legacy `configurationFromString` helper remains precomputed-only. Rules are evaluated locally for the | |
| current context, so you can change users or targeting attributes without fetching a new configuration: | |
| Use `coreConfigurationFromString` from the `/rules-based` entry point to parse rules-based configurations or configurations containing both precomputed data and rules. `configurationFromString` supports only precomputed configurations. Rules are evaluated locally for the current context, so you can change users or targeting attributes without fetching another configuration: |
| For combined configurations, matching precomputed data takes precedence; rules are used when the | ||
| precomputed context does not match. A valid capability can still be used if the other is malformed. |
There was a problem hiding this comment.
| For combined configurations, matching precomputed data takes precedence; rules are used when the | |
| precomputed context does not match. A valid capability can still be used if the other is malformed. | |
| For configurations containing both precomputed data and rules, matching precomputed data takes precedence. The provider uses rules when the precomputed context does not match. If either part is malformed, the provider can still use the valid part. |
|
@domalessi I'll open up a follow-up PR with your suggestions, but I wanted to get this PR through to unblock a separate PR |
What does this PR do?
Adds offline rules-based evaluation to React Native. Before this PR,
DatadogOfflineOpenFeatureProvidercould only serve precomputed configurations.DatadogCoreProvider, ported from the browser SDK. It evaluates precomputed or rules-based configurations you supply manually, entirely in JavaScript with@datadog/flagging-core. It doesn't useDdFlags, doesn't fetch, and sends no telemetry.DatadogOfflineOpenFeatureProvidernow serves rules-based configurations too. It hands evaluation toDatadogCoreProviderinstead of keeping its own copy of the logic. Its public API and released precomputed behavior are unchanged: context normalization and adoption, named native clients, and native exposure/RUM tracking.Either provider can now be given an offline rules-based configuration (parsed with
coreConfigurationFromStringfrom the/rules-basedentry point).Motivation
FFL-2837: support manually supplied rules-based configurations offline in React Native, without keeping a second copy of the flag evaluation logic.
Decision: tracking hooks are deferred
The browser SDK's building-blocks model pairs
DatadogCoreProviderwith tracking hooks that customers add themselves:This PR does not add those hooks. React Native has a single native tracking call,
NativeDdFlags.trackEvaluation(...). Native configuration (trackExposures,rumIntegrationEnabled) decides whether that call sends an exposure, records a RUM evaluation, does both, or does neither. Two independent hooks built on that one call can't be enabled or disabled separately, and adding both would track every evaluation twice.Until then: use
DatadogOfflineOpenFeatureProviderwhen you need exposures or RUM. It runs the same core evaluation (precomputed and rules-based) and tracks through the native SDK, carrying the full tracking payload, including precomputedextraLogging. UseDatadogCoreProviderwhen you want evaluation without telemetry.Later: we'll add browser-style split hooks (
createDatadogExposureLoggingHook/createDatadogRumTrackingHook) once dd-sdk-ios and dd-sdk-android can track exposures and RUM separately. That change is additive and won't modifyDatadogCoreProvider. We're intentionally not shipping a combined native tracking hook in the meantime: it would duplicate the offline provider, and we'd have to deprecate it once the split hooks exist.Additional Notes
DatadogOfflineOpenFeatureProviderneeds the matching@datadog/mobile-react-nativeversion. With an older core SDK, precomputed-only behavior is unchanged. If usable rules are supplied, the provider logs a warning and enters an error state, so it never silently ignores the rules or keeps serving a stale configuration.nullcontext attributes. Anullattribute value in the evaluation context used to reach the native bridge, wherebuildEvaluationContextcalled.toString()on it and threw aNullPointerException. Android now omitsnullattributes. This should be called out in the changelog for the release.nullattributes as explicit null values, while Android drops them. A follow-up PR will either drop them on iOS too, so both platforms match, or document the difference.Review checklist (to be filled by reviewers)