Skip to content

[FFL-2837] Add core provider and offline rules evaluation - #1456

Merged
greghuels merged 22 commits into
developfrom
greg.huels/FFL-2837/offline-dynamic
Oct 1, 2026
Merged

greghuels merged 22 commits into
developfrom
greg.huels/FFL-2837/offline-dynamic

Conversation

@greghuels

@greghuels greghuels commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

What does this PR do?

Adds offline rules-based evaluation to React Native. Before this PR, DatadogOfflineOpenFeatureProvider could only serve precomputed configurations.

  • New 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 use DdFlags, doesn't fetch, and sends no telemetry.
  • DatadogOfflineOpenFeatureProvider now serves rules-based configurations too. It hands evaluation to DatadogCoreProvider instead 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 coreConfigurationFromString from the /rules-based entry 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 DatadogCoreProvider with tracking hooks that customers add themselves:

client.addHooks(
    createDatadogExposureLoggingHook(),
    createDatadogRumTrackingHook()
);

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 DatadogOfflineOpenFeatureProvider when 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 precomputed extraLogging. Use DatadogCoreProvider when 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 modify DatadogCoreProvider. 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

  • Rules-based evaluation in DatadogOfflineOpenFeatureProvider needs the matching @datadog/mobile-react-native version. 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.
  • Fixes an Android crash with null context attributes. A null attribute value in the evaluation context used to reach the native bridge, where buildEvaluationContext called .toString() on it and threw a NullPointerException. Android now omits null attributes. This should be called out in the changelog for the release.
    • Known platform difference (follow-up): iOS still forwards null attributes 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.
  • Validated locally: 200+ passing tests across both packages, type checks, lint, and build verification for all supported module formats.
  • Android/iOS device system tests pass.

Review checklist (to be filled by reviewers)

  • Feature or bugfix MUST have appropriate tests
  • Make sure you discussed the feature or bugfix with the maintaining team in an Issue
  • Make sure each commit and the PR mention the Issue number (cf the CONTRIBUTING doc)
  • If this PR is auto-generated, please make sure also to manually update the code related to the change

@datadog-datadog-prod-us1-2

datadog-datadog-prod-us1-2 Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Tests

✅ All CI checks and tests passed.

🎉 All green!

🧪 All tests passed
❄️ No new flaky tests detected

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 0537205 | Docs | View more details | Give us feedback!

Comment thread packages/react-native-openfeature/src/index.ts Outdated
Comment thread packages/react-native-openfeature/src/offlineEvaluation.ts Outdated
Comment thread packages/react-native-openfeature/README.md Outdated
Comment thread packages/react-native-openfeature/src/offlineProvider.ts
Comment thread packages/react-native-openfeature/src/offlineEvaluation.ts Outdated
Comment thread packages/core/src/flags/FlagsClient.ts
Comment thread packages/react-native-openfeature/src/offlineProvider.ts
Comment thread packages/react-native-openfeature/src/index.ts Outdated
Comment thread packages/react-native-openfeature/src/offlineEvaluation.ts
Comment thread packages/react-native-openfeature/src/offlineProvider.ts Outdated
Comment thread packages/react-native-openfeature/src/offlineEvaluation.ts Outdated
Comment thread packages/react-native-openfeature/package.json
Comment thread packages/react-native-openfeature/src/offlineEvaluation.ts Outdated
Comment thread packages/react-native-openfeature/README.md
Comment thread packages/react-native-openfeature/src/offlineEvaluation.ts
Comment thread packages/react-native-openfeature/README.md
@greghuels
greghuels marked this pull request as ready for review October 1, 2026 14:15
Copilot AI balanced review requested due to automatic review settings October 1, 2026 14:15
@greghuels
greghuels requested review from a team as code owners October 1, 2026 14:15
@greghuels
greghuels requested review from btthomas and hhan2024 and removed request for a team October 1, 2026 14:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 DatadogCoreProvider and opt-in rules-based parsing.
  • Bridges offline evaluation through named native FlagsClient instances.
  • 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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

The cross-package evaluator bridge and native tracking changes warrant final human validation despite extensive coverage.

Review effort: Balanced
Findings: None

Comment thread packages/react-native-openfeature/src/offlineProvider.ts Outdated
Comment thread packages/react-native-openfeature/src/datadogCoreProvider.ts Outdated
@marco-saia-datadog

Copy link
Copy Markdown
Member

Looks good to me! 👍 I have left a few notes, and a small change request :)

greghuels and others added 2 commits October 1, 2026 10:46
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>
Copilot AI balanced review requested due to automatic review settings October 1, 2026 15:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

The cross-package evaluation bridge and native tracking behavior warrant final human validation.

Review effort: Balanced
Findings: None

@btthomas btthomas left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread packages/core/src/flags/FlagsClient.ts
Comment thread packages/react-native-openfeature/package.json
Comment thread packages/react-native-openfeature/src/datadogCoreProvider.ts
Comment thread packages/core/src/flags/FlagsClient.ts
greghuels and others added 2 commits October 1, 2026 13:49
…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>
Copilot AI balanced review requested due to automatic review settings October 1, 2026 18:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

The cross-package evaluator bridge, lifecycle changes, backward compatibility, and native tracking integration warrant final human validation.

Review effort: Balanced
Findings: None

@domalessi domalessi left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specify the minimum versions of @datadog/mobile-react-native and @datadog/mobile-react-native-openfeature required for rules-based evaluation.

Comment on lines +343 to +348
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`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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`.

Comment on lines +294 to +297
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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.

Comment on lines +323 to +324
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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.

Comment on lines +328 to +330
- 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).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- 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.

Comment on lines +335 to +339
- 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`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- 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`.

Comment on lines +358 to +360
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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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:

Comment on lines +390 to +391
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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.

@greghuels
greghuels merged commit f085436 into develop Oct 1, 2026
16 checks passed
@greghuels
greghuels deleted the greg.huels/FFL-2837/offline-dynamic branch October 1, 2026 19:58
@greghuels

Copy link
Copy Markdown
Contributor Author

@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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants