Skip to content

feat(flags): FlagsClient.setConfiguration + context matching [PR2] (FFL-2688) - #1331

Merged
btthomas merged 13 commits into
blake.thomas/FFL-2666from
blake.thomas/FFL-2666-PR2
Jul 22, 2026
Merged

feat(flags): FlagsClient.setConfiguration + context matching [PR2] (FFL-2688)#1331
btthomas merged 13 commits into
blake.thomas/FFL-2666from
blake.thomas/FFL-2666-PR2

Conversation

@btthomas

@btthomas btthomas commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Offline feature flags — related PRs


Summary

Adds the core offline feature-flag API to FlagsClient (@datadog/mobile-react-native): loading a supplied configuration and evaluating it with no network request.

  • setConfiguration(config) — decodes a precomputed configuration once (from configurationFromString) into the flag cache and adopts the context embedded in the configuration, so offline evaluation works immediately with no fetch.
  • setEvaluationContextWithoutFetching(context) — records the active context and reconciles the loaded configuration against it, with no native request (the offline counterpart to setEvaluationContext).
  • resetEvaluationContextWithoutFetching() — clears any externally-set context so the loaded configuration is served against its embedded context again.

All three return a discriminated ConfigurationResult ({ status: 'ready' } | { status: 'error'; errorCode }) so the OpenFeature provider (#1332) can surface the correct state. Core-only, no native changes: evaluation and per-flag exposure/RUM tracking already run off the JS-provided flag data.

Context handling: a mismatch is an error

A precomputed configuration is a single-subject snapshot, and offline never fetches, so it can only be served against the context it was computed for. A different runtime context cannot be served, so it is now an error (this replaces the earlier "ignore the mismatch and keep serving the snapshot" behavior). The reconcile result is error with a precise errorCode, surfaced at evaluation as the coded default value with reason: 'ERROR' and no exposure tracking:

  • INVALID_CONTEXT — the active context does not match the snapshot (compared after normalization).
  • PROVIDER_NOT_READY — an offline operation ran with no configuration loaded (distinct from the online path's none, so no-config no longer misreports FLAG_NOT_FOUND).
  • GENERAL — the loaded configuration is unusable. This PR also hardens decodePrecomputedFlags to reject a structurally malformed response envelope (null / array / missing data.attributes.flags), alongside unsupported (obfuscated) payloads. configurationFromString is lenient, so a distinct PARSE_ERROR is not produced this iteration; everything unusable is GENERAL.

External vs. embedded context. Only an app-set external context is matched against the snapshot; adopting a configuration's own embedded context is never treated as an override. So replacing one precomputed snapshot with another for a different subject (with no external context set) stays ready and adopts the new embedded context — exposures are attributed to it.

Load validity is stored structurally (LoadedConfigurationState) and the payload is decoded once at load. A later context change reconciles against the stored snapshot without re-decoding, and can never promote an invalid (or replaced-with-invalid) load back to ready.

Empty context = no external override. resetEvaluationContextWithoutFetching() (used by the provider for clearContext() / an empty context) drops the override and re-adopts the embedded context, so clearing/omitting context is order-independent and recovers a prior mismatch.

Offline → online is unsupported on one client, but degrades safely. Entering online mode (setEvaluationContext) discards any loaded offline overlay before fetching, with a warning, so a failed online fetch serves coded defaults rather than a stale offline snapshot. (Online-only clients have no overlay and keep their last-known flags on a failed fetch, as before.)

Two orthogonal axes still ride one setConfiguration API (see the RFC's CoreProvider): online vs offline (fetch vs never-fetch, a property of the provider) and precomputed vs rules (configuration kind; rules-based per-context evaluation is future work, handled before the precomputed guard).

Hybrid apps (intentional limitation). The offline configuration is applied only in the JS layer; it is not propagated to the native SDKs, so a hybrid app that also reads flags natively will have inconsistent configuration between JS and native. A deliberate, documented limitation for now.

Tests

The flags/ suite passes, covering: order-independent load/context (config-first and context-first); a differing context erroring with INVALID_CONTEXT and serving defaults with no exposure; snapshot-A→snapshot-B replacement for a different subject staying ready (with exposure attributed to B); unusable / malformed-envelope / obfuscated configs → GENERAL; no-config offline op → PROVIDER_NOT_READY; the replacement invariant (an invalid replacement is never resurrected by a later context change); reset recovery; the offline→online overlay drop on a failed fetch; context-agnostic configs; and the online path. Lint and tsc are clean.

Copilot AI 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.

Pull request overview

Adds the core JS-side offline feature-flags capability to the React Native SDK by letting a caller load a precomputed configuration into FlagsClient and evaluate flags without any native/network fetch, including strict context matching.

Changes:

  • Introduces FlagsClient.setConfiguration() and FlagsClient.setEvaluationContextWithoutFetching() plus configuration status handling (INVALID_CONTEXT / PROVIDER_NOT_READY).
  • Adds wire-context normalization + exact context matching utilities for offline/precomputed configs.
  • Hardens evaluation-context attribute handling against prototype-key issues and adds targeted unit tests.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/core/src/flags/types.ts Adds INVALID_CONTEXT error code for evaluation failures.
packages/core/src/flags/internal.ts Builds processed context attributes via Map + Object.fromEntries to safely handle reserved keys.
packages/core/src/flags/FlagsClient.ts Implements offline configuration loading, context reconciliation without fetching, and new error surfacing.
packages/core/src/flags/configuration/index.ts Exports new context utilities from the configuration module.
packages/core/src/flags/configuration/context.ts Adds normalizeWireContext + contextMatchesConfiguration for context comparison.
packages/core/src/flags/configuration/tests/context.test.ts Tests wire-context normalization and matching semantics.
packages/core/src/flags/tests/internal.test.ts Tests processEvaluationContext handling of non-primitive and __proto__ attributes.
packages/core/src/flags/tests/FlagsClient.test.ts Adds coverage for offline configuration loading, mismatch handling, replacement, and no-fetch context updates.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +125 to +135
setEvaluationContextWithoutFetching = (
context: EvaluationContext
): void => {
this.evaluationContext = processEvaluationContext(context);

// Re-evaluate a loaded offline configuration against the new context. Readiness
// when no configuration is loaded yet is the provider's concern.
if (this.loadedConfiguration) {
this.applyConfiguration();
}
};
['__proto__']: 'x'
});

// The reserved key is safely discarded; the prototype is untouched.
Comment thread packages/core/src/flags/FlagsClient.ts Outdated
Comment on lines +185 to +198
let decoded: Record<string, FlagCacheEntry>;
try {
decoded = decodePrecomputedFlags(precomputed.response);
} catch (error) {
this.flagsCache = {};
this.configurationStatus = 'invalid';
if (error instanceof Error) {
InternalLog.log(
`Unsupported flags configuration for '${this.clientName}': ${error.message}`,
SdkVerbosity.WARN
);
}
return;
}
Comment thread packages/core/src/flags/FlagsClient.ts Outdated
* Reconcile the loaded configuration against the active evaluation context and
* (re)compute the servable flag cache and configuration status.
*/
private applyConfiguration = (): void => {

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.

As mentioned on the docs PR here: #1326 (comment)

Not propagating the Flags configuration to the native SDK underneath effectively means that offline init does not support hybrid apps, as apps that fetch the Flags config from the native side won't have the same configuration as the JS layer. This introduces an inconsistency that at the very least needs to be accounted and acknowledged for, especially if the initial implementation won't rely on any changes coming from the native SDKs.

@btthomas
btthomas force-pushed the blake.thomas/FFL-2666-PR1 branch from 2d20825 to 1009719 Compare July 8, 2026 14:50
@btthomas
btthomas force-pushed the blake.thomas/FFL-2666-PR2 branch from 8cce4b5 to 3bd5e6e Compare July 8, 2026 14:51
@btthomas
btthomas force-pushed the blake.thomas/FFL-2666-PR1 branch from 199fb80 to 3f7c10a Compare July 16, 2026 17:43
@btthomas
btthomas requested review from a team as code owners July 16, 2026 17:43
@btthomas
btthomas requested review from pavlokhrebto and sameerank and removed request for a team July 16, 2026 17:43
@btthomas
btthomas force-pushed the blake.thomas/FFL-2666-PR2 branch 2 times, most recently from 752c7a6 to 22ebc03 Compare July 16, 2026 18:34
@datadog-datadog-prod-us1-2

datadog-datadog-prod-us1-2 Bot commented Jul 16, 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: f7e019f | Docs | Datadog PR Page | Give us feedback!

@btthomas
btthomas force-pushed the blake.thomas/FFL-2666-PR2 branch from 6133947 to c63c75e Compare July 17, 2026 14:40
Comment thread packages/core/src/flags/FlagsClient.ts Outdated
@btthomas
btthomas force-pushed the blake.thomas/FFL-2666-PR2 branch from c63c75e to c5831d8 Compare July 20, 2026 21:28
btthomas and others added 2 commits July 22, 2026 10:31
…2688)

Add offline configuration loading to the core FlagsClient:

- setConfiguration(config): decodes a precomputed configuration into the flag
  cache. When no context is set yet, it adopts the configuration's embedded
  context (implicit set) so offline evaluation works with no native fetch. When
  a context is already set, the configuration's context must match it.
- Context matching: new configuration/context.ts normalizes the flat wire
  context through the same processEvaluationContext as the active context, then
  deep-compares — a config with no embedded context is context-agnostic.
- Mismatch -> serve no values and report INVALID_CONTEXT (added to FlagErrorCode);
  empty/unsupported/invalid config -> PROVIDER_NOT_READY (not FLAG_NOT_FOUND).
- A subsequent native setEvaluationContext fetch supersedes the offline config.

Tests cover context normalization/matching and the setConfiguration flows
(implicit-set no-fetch, match/mismatch against an explicit context, invalid
config, and fetch supersession).

fetchPolicy (PR3) and the OpenFeature provider lifecycle (PR4) are unchanged
here. The parse-failure vs valid-but-empty distinction (L2) remains for a
follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tests)

Review follow-ups on PR2:
- #7: build the processed evaluation context with a Map + Object.fromEntries so
  reserved keys like "__proto__" (notably a "__proto__": null attribute, which a
  plain assignment would have used to null the object's prototype) are handled as
  data without prototype manipulation.
- #1: mark the forward-compat seam in applyConfiguration so a future server/rules
  configuration is not rejected as "invalid" (rules configs are context-agnostic).
- #3/#4/#5: document the deferred seams — fetch-failure/staleness fallback and the
  NEVER re-match belong to the fetch-policy step; the PROVIDER_ERROR provider event
  belongs to the OpenFeature provider step.
- #6: add tests for a context-agnostic configuration, configuration replacement,
  and the __proto__ context-attribute / proto-null safety cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
btthomas and others added 11 commits July 22, 2026 10:31
The offline counterpart to setEvaluationContext: records the active context and
reconciles a configuration loaded via setConfiguration against it (context
matching for a precomputed config) with no native request. This is the core
seam an offline OpenFeature provider uses to update context on
initialize/onContextChange without ever fetching from the CDN — replacing the
previously-planned fetchPolicy flag with a provider-level choice.

Tests cover the no-fetch reconcile (match serves, context-change mismatch ->
INVALID_CONTEXT) and assert the native fetch is never called.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…omputed (PR2 review)

An offline precomputed configuration is a single-subject snapshot and offline never fetches, so a runtime evaluation context that differs from the one the snapshot was computed for can no longer be honored. Instead of the previous mismatch -> PROVIDER_ERROR/INVALID_CONTEXT behavior, we now ignore the differing context (keep serving the snapshot against its embedded context) and log a warning once, on the context change. Per-context evaluation is the job of the future rules-based configuration.

Drops the 'mismatch' ConfigurationStatus and the getDetails INVALID_CONTEXT branch. Fixes the stale-cache case (Copilot review): setEvaluationContextWithoutFetching now clears the cache when no configuration is loaded. Un-exports contextMatchesConfiguration from the configuration module index (now an internal helper imported directly by FlagsClient). Corrects the __proto__ context test comment to only assert what it verifies (no prototype pollution).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to the ignore-a-mismatched-context change: the setEvaluationContextWithoutFetching doc no longer describes 'context matching' (it re-applies and ignores a differing precomputed context), and the applyConfiguration forward-compat note drops the reference to the removed 'server' wire branch / ParsedFlagsConfiguration while keeping the (still-valid) rules seam.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… tests)

- Remove the unused 'INVALID_CONTEXT' error code from FlagErrorCode: it was added for the old context-mismatch path, which the ignore-and-warn change removed, so the SDK never emits it.

- Guard a non-string wire targetingKey in normalizeWireContext (the wire is untrusted): a non-string value is treated as absent instead of landing in a string-typed field.

- Correct the 'warn once' comment (it warns on each differing context change, which is the intended behavior).

- Add a test asserting exposures are attributed to the snapshot's embedded context, not an ignored runtime context.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ookups (PR2 review)

flagsCache was a plain object indexed by flag key, so a key equal to an inherited property name ('toString', 'constructor', '__proto__') resolved an Object.prototype member instead of undefined, bypassing the not-found guard and returning TYPE_MISMATCH rather than FLAG_NOT_FOUND. Use a Map with .get(): the online native result is wrapped via new Map(Object.entries(result)), the offline path consumes the Map from decodePrecomputedFlags directly, and lookups never touch the prototype chain. Fixes it for both online and offline paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… review)

Replace the bare string union with an `as const` object + derived union so
assignment/comparison sites read ConfigurationStatus.None/Ready/Invalid —
self-documenting and typo-safe — while the type stays an erasable string union
(no TS enum runtime code, friendlier to Babel and tree-shaking).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Switch the offline precomputed flow from "ignore a mismatched runtime
context and keep serving the snapshot" to treating it as an error that
serves coded defaults, so the OpenFeature provider can surface the
correct error state.

- Widen the offline APIs to return a discriminated ConfigurationResult
  (ready, or error with a precise errorCode) so the reason propagates:
  INVALID_CONTEXT (mismatch), PROVIDER_NOT_READY (no config loaded),
  GENERAL (unusable/undecodable/unsupported config).
- Store load validity structurally (LoadedConfigurationState) and decode
  once at load, so a later context change can never promote an invalid
  load to ready and never re-decodes.
- Add resetEvaluationContextWithoutFetching (clear the external override
  and reconcile) so clearing/omitting context re-adopts the embedded
  context.
- getDetails serves coded defaults with the precise errorCode on error;
  no exposure tracking while serving defaults.
- Add INVALID_CONTEXT and GENERAL to FlagErrorCode; drop the stale
  fetch-policy comments in setEvaluationContext.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…subject

Track the app-set external context separately from the effective evaluation
context. Previously, adopting a configuration's embedded context stored it in
`evaluationContext`, so loading a replacement snapshot for a different subject
was mistaken for a mismatch against an "already set" context and returned
INVALID_CONTEXT — even though the app never set a context.

reconcile() now checks only an explicit `externalContext` for a mismatch; the
effective context is `externalContext ?? embedded`. Replacing snapshot A with
snapshot B (or loading a subject-bound config after a context-agnostic one)
with no external override stays `ready`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Using one FlagsClient for both the offline and online providers is
unsupported, but it should degrade safely rather than serve stale data.
Previously the offline overlay was cleared only after a *successful*
native fetch, so a failed online fetch left the offline snapshot in
place and kept serving (and tracking) it.

setEvaluationContext now discards any offline overlay up front (with a
warning) before fetching, so a failed fetch falls back to coded defaults
instead of the stale offline snapshot. Online-only clients have no
overlay, so their keep-last-known-flags-on-failure behavior is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
decodePrecomputedFlags treated a null/non-object envelope or a missing
`data.attributes.flags` as an empty flag map, so a JSON-valid but
structurally malformed configuration decoded to an empty — but "ready" —
snapshot and evaluations returned FLAG_NOT_FOUND instead of an error.

Validate that `flags` is a plain object (rejecting null, arrays, and
missing envelopes) and throw UnsupportedConfigurationError otherwise, so
setConfiguration classifies it as GENERAL. A genuinely empty `flags: {}`
is still accepted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nvelope

- After replacing snapshot A with B (different subject, no external context),
  assert trackEvaluation is called with B's embedded targetingKey, so a
  regression that updated the flags but kept A's context would be caught.
- Add a public-boundary test: a wire whose precomputed response omits
  `data.attributes.flags` reconciles to GENERAL and serves coded defaults.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@btthomas
btthomas force-pushed the blake.thomas/FFL-2666-PR2 branch from 320d49f to f7e019f Compare July 22, 2026 14:31
Base automatically changed from blake.thomas/FFL-2666-PR1 to blake.thomas/FFL-2666 July 22, 2026 14:50
@btthomas
btthomas merged commit 10fb8e7 into blake.thomas/FFL-2666 Jul 22, 2026
6 of 8 checks passed
@btthomas
btthomas deleted the blake.thomas/FFL-2666-PR2 branch July 22, 2026 14:50
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.

3 participants