feat(flags): FlagsClient.setConfiguration + context matching [PR2] (FFL-2688) - #1331
Conversation
5a7a06e to
8cce4b5
Compare
de2a6ec to
2d20825
Compare
There was a problem hiding this comment.
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()andFlagsClient.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.
| 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. |
| 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; | ||
| } |
| * Reconcile the loaded configuration against the active evaluation context and | ||
| * (re)compute the servable flag cache and configuration status. | ||
| */ | ||
| private applyConfiguration = (): void => { |
There was a problem hiding this comment.
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.
2d20825 to
1009719
Compare
8cce4b5 to
3bd5e6e
Compare
199fb80 to
3f7c10a
Compare
752c7a6 to
22ebc03
Compare
🎉 All green!🧪 All tests passed 🔗 Commit SHA: f7e019f | Docs | Datadog PR Page | Give us feedback! |
6133947 to
c63c75e
Compare
c63c75e to
c5831d8
Compare
…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>
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>
320d49f to
f7e019f
Compare
Offline feature flags — related PRs
FlagsClient.setConfiguration): feat(flags): FlagsClient.setConfiguration + context matching [PR2] (FFL-2688) #1331 ← you are hereSummary
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 (fromconfigurationFromString) 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 tosetEvaluationContext).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
errorwith a preciseerrorCode, surfaced at evaluation as the coded default value withreason: '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'snone, so no-config no longer misreportsFLAG_NOT_FOUND).GENERAL— the loaded configuration is unusable. This PR also hardensdecodePrecomputedFlagsto reject a structurally malformed response envelope (null / array / missingdata.attributes.flags), alongside unsupported (obfuscated) payloads.configurationFromStringis lenient, so a distinctPARSE_ERRORis not produced this iteration; everything unusable isGENERAL.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
readyand 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 toready.Empty context = no external override.
resetEvaluationContextWithoutFetching()(used by the provider forclearContext()/ 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
setConfigurationAPI (see the RFC'sCoreProvider): 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 withINVALID_CONTEXTand serving defaults with no exposure; snapshot-A→snapshot-B replacement for a different subject stayingready(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 andtscare clean.