Skip to content

docs: plan for provider.setConfiguration offline init (FFL-2666)#1326

Draft
btthomas wants to merge 22 commits into
developfrom
blake.thomas/FFL-2666
Draft

docs: plan for provider.setConfiguration offline init (FFL-2666)#1326
btthomas wants to merge 22 commits into
developfrom
blake.thomas/FFL-2666

Conversation

@btthomas

@btthomas btthomas commented Jul 6, 2026

Copy link
Copy Markdown

Offline feature flags — related PRs


Summary

Adds provider_set_configuration.md, the design doc for offline feature-flag initialization in the React Native SDK: letting a customer load a flag configuration they fetched themselves and evaluate it exactly as if it came from the edge CDN, via

configurationFromString(wire) -> setConfiguration(configuration) -> evaluate(...)

and a dedicated offline OpenFeature provider that never touches the network. Documentation only — no code.

It covers the scope (precomputed, static single-context) and non-goals, the ConfigurationWire/precomputed formats, context matching, the offline provider, and how the work breaks down. See the doc for detail.

btthomas and others added 5 commits July 6, 2026 16:49
Adds provider_set_configuration.md describing the plan to expose a JS
setConfiguration path so a customer-supplied precomputed ConfigurationWire
loads and evaluates exactly like CDN-fetched assignments. Planning only, no
implementation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ConfigurationWire is version 1 (per updated spec and shipped wire.ts), and
the envelope now reserves a server/rules branch alongside precomputed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Parser returns an empty config on invalid input or unknown version, matching
the shipped wire.ts. Predictable failure surfaces at the provider layer.

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

Adopt the openfeature-js-client PrecomputedFlag shape (matches RN's
FlagCacheEntry, decoder ~1:1) instead of the OpenFeature-aligned proposal
page. Flags the two-format discrepancy to confirm with the flags team.

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

openfeature-js-client is not a dependency here, so port its pure wire.ts and
configMatchesContext helpers into RN core to preserve the native RUM/exposure
path. Documents the context-agnostic matching nuance.

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

btthomas commented Jul 6, 2026

Copy link
Copy Markdown
Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

btthomas and others added 3 commits July 6, 2026 19:07
Nothing here is confirmed or decided yet, so rename the Approach and Wire
format headings to avoid implying finality.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A staging /precompute-assignments sample (example.json) shows the
PrecomputedFlag-style shape (typed variationValue, RFC3339 createdAt,
obfuscated:false), matching RN's FlagCacheEntry. Update the format block and
notes to reference it and soften remaining certainty wording.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Explains that native exposure/RUM tracking is per-flag and parameter-driven
(no dependency on a prior CDN fetch), so offline init stays in JS. Notes that
FlagsClient.setEvaluationContext currently triggers a native CDN fetch and
overwrites the cache, so it must branch when an offline config is loaded
(FFL-2688).

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

@aarsilv aarsilv 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.

Nice work! 💪 Digging what I'm seeing! I've attempted to answer your open questions. Feel free to get a head start on prototyping this out while people comment on your plan. 🐎


## Open items (non-blocking)

- Check with the flags team whether the `PrecomputedFlag`-style shape seen in the sample

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The CDN currently returns the backend PrecomputedAssignments / PrecomputedFlag shape

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

So is my inlined example above correct?

Comment thread provider_set_configuration.md Outdated
- Check with the flags team whether the `PrecomputedFlag`-style shape seen in the sample
(`example.json`) is stable across environments/versions, vs the OpenFeature-aligned proposal
page (FFL-2687).
- Precomputed context-mismatch behavior: default value vs `PROVIDER_NOT_READY` vs error

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

If an error we should return default value but with a PROVIDER_NOT_READY code (@dd-oleksii can fact check me here)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Above, you just said we should have a PROVIDER_ERROR on mismatch. Which is preferred?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Comment thread provider_set_configuration.md Outdated
page (FFL-2687).
- Precomputed context-mismatch behavior: default value vs `PROVIDER_NOT_READY` vs error
(RFC open question).
- `setConfiguration` sync vs `Promise`-returning (JS-only work is synchronous, but a Promise

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I don't see a huge downside to setConfiguration being asynchronous as I imagine being called infrequently and in a non blocking way.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Our mobile customers are more focused on startup performance, correct? So that means we should make setConfiguration async, so they can do other app init things while our config is loaded, correct? Is that a good or bad assumption?

Comment thread provider_set_configuration.md Outdated
- Precomputed context-mismatch behavior: default value vs `PROVIDER_NOT_READY` vs error
(RFC open question).
- `setConfiguration` sync vs `Promise`-returning (JS-only work is synchronous, but a Promise
keeps parity with `setEvaluationContext` and forward-compat for rules).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Side note, I am concerned about this being async due to race conditions. See: https://docs.google.com/document/d/12i0O__eHWWj1q5qSXID7VILiWxacHclLef-Y5oSea58/edit?disco=AAAB_VOdxLw

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

That comment is related to dynamic (multi-user ruleset) when setContext passes in a new context. Does that concern apply here? Do we need to get it right in this stack of PRs or can it wait until we implement the dynamic context offline init?

Comment thread provider_set_configuration.md Outdated
Comment on lines +115 to +117
- **Obfuscation is not supported** in the DD precomputed format (the sample response carries
`obfuscated: false`) — parsing is plain JSON → object mapping (no key hashing, no
base64/salt decoding).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Future us! Since the current edge compute doesn't do this I think its fine to punt to sometime in the future (or never)

Comment thread provider_set_configuration.md Outdated
Customers may call `setConfiguration` and `setEvaluationContext` in either order. The
`FlagsClient` holds the **loaded configuration** (carrying its embedded `context`) and the
**active evaluation context** independently. The servable `flagsCache` is only populated
when the two **match**; the match is re-validated on **both** calls. On mismatch, values are

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

On mismatch, values are
not served.

Should we instead return coded default value with a PROVIDER_ERROR code?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

errorCode: INVALID_CONTEXT is in the OpenFeature spec. That's the closest we have to "mismatch".

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

configuration is present: record the context without invoking the native fetch and without
overwriting the config-populated cache. This is a JS-only change and lives in FFL-2688.

## Work breakdown

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Is plan to unleash agents for stacked PRs here? Note you can have agents use tools like git machete or jj--useful for propagating changes up the stack when iterating on lower down PRs.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

That is exactly my plan. A stack of PRs optimized for review while we can test the top of the stack end-to-end.

btthomas and others added 2 commits July 7, 2026 08:45
Add a Non-goals section (fetchPrecomputedConfiguration, precomputeConfiguration,
rules-based eval out of scope), note the fetch source could be the Datadog/Fastly
edge CDN or a customer proxy, and specify context mismatch as PROVIDER_ERROR plus
an INVALID_CONTEXT evaluation error code.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
setEvaluationContext always fetches from the CDN today with no toggle. Add a
fetchPolicy (ALWAYS default / NEVER / ON_MISMATCH) set at enable() with a
per-getClient() override; build ALWAYS + NEVER now and defer ON_MISMATCH.
Tracked as FFL-2718.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@btthomas btthomas requested a review from dd-oleksii July 7, 2026 13:18
Group the seven sub-tasks into four stacked, self-contained PRs (unit tests
co-located, no trailing test-only PR) and document the ordering rationale.
FFL-2691 is repurposed to RUM FIT integration/e2e coverage (offline-loaded
flag -> RUM parity). Jira encodes the order via Blocks links.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@btthomas btthomas requested a review from sbarrio July 7, 2026 13:53
@datadog-datadog-prod-us1-2

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

btthomas and others added 6 commits July 7, 2026 10:35
The parsed-wire config type must not reuse FlagsConfiguration, which already
names the enable() options type. Use ParsedFlagsConfiguration for the
configurationFromString/setConfiguration surface.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a configuration-kind/evaluation-mode concept chosen by which wire branch
is populated (precomputed vs future server/rules), keep fetchPolicy as the
network axis only, and gate the context match/mismatch-error on the
precomputed kind so a future rules config (context-agnostic) is not rejected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The decoder must read attributes.obfuscated and fail predictably
(unsupported -> PROVIDER_ERROR) instead of mis-mapping hashed keys as flag
names if the CDN ever enables obfuscation. This is the seam for a future
obfuscated format.

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

Pass fetchPolicy inside an options object so it can grow (ttl,
staleWhileRevalidate) without an API break, and record that the future rules
(UFC) evaluator should be depended on from a shared core rather than
re-ported, to avoid silent assignment drift.

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

Capture the plan-review silent-failure risks as a per-subtask acceptance
checklist: variationValue stringification, wire-context normalization before
matching, NEVER sets context synchronously, provider event ordering, and the
iOS int/double parity note.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add an "Executing a step" section: what to read (this doc, the RFCs, the Jira
ticket + acceptance checklist), where to ground in code/formats, the required
shape of the per-step implementation plan (explicit files, signatures, return
types, tests), the guardrails, and a reusable prompt. Lets a fresh-context
agent execute any PR step consistently.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment on lines +44 to +46
fetches + parses and returns a **serialized snapshot** (`Record<string, FlagCacheEntry>`).
4. JS caches the snapshot in `flagsCache`. **All evaluation already happens in JS** against
that cache.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

aside: while we're here, shall we rename FlagCacheEntry and flagsCache? (because it's not a cache and calling it cache is confusing)

I believe in other sdks we call it either "precomputed flag" or "flag configuration"

Comment on lines +61 to +64
- **Avoid the `FlagsConfiguration` name collision.** That identifier is already the `enable()`
options type (`packages/core/src/flags/types.ts`). Name the parsed-wire config type distinctly
— this plan uses **`ParsedFlagsConfiguration`** — so `configurationFromString` /
`setConfiguration` don't overload the existing type.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

"Parsed" seems like a bad describer here — it's not something that we want to highlight. Also having both FlagsConfiguration and an unrelated ParsedFlagsConfiguration makes it way more confusing. I don't have a good suggestion though 😢 having FlagsConfiguration already used is unfortunate

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

What should we do? FlagsConfiguration is already defined/exported as the parameter for the public enable() method.

Comment thread provider_set_configuration.md Outdated
Comment on lines +67 to +69
- Add a **`fetchPolicy`** (`ALWAYS` default / `NEVER` / `ON_MISMATCH`) set at `enable()` with a
per-`getClient()` override, so customers can turn off the fetch-on-`setEvaluationContext`.
Offline init needs `NEVER`; see [Fetch policy](#fetch-policy).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'd start with ALWAYS and NEVER. "On mismatch" is not well-defined yet (e.g. we don't have a way to produce context-agnostic config). We might also add "poll" later.

Proposing to go with the minimal set of options for now because it's easy to add options in the future but it's hard to remove them

Comment on lines +70 to +78
- **Port, don't depend.** `openfeature-js-client` is not a dependency here (only upstream
`@openfeature/core` + `@openfeature/web-sdk` are). Port its small pure helpers —
`wire.ts` (`configurationFromString`/`configurationToString`) and `configMatchesContext` —
into RN core rather than depending on the package, whose provider/exposure-logging would
bypass RN's native RUM/exposure path. **But for the future rules (UFC) evaluator and any
obfuscation logic, prefer *depending* on a shared platform-agnostic core (e.g.
`@datadog/flagging-core`) rather than re-porting** — hand-porting an evolving evaluator risks
silent assignment drift. Keep the ported helpers behind a thin internal module boundary so a
later port → dependency swap stays contained.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Consider moving precomputed configuration types, configuration wire parsing/serializing functions, and precomputed "evaluator" into core

UPD: it looks like they are already there — anything that prevents us from reusing the core?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

anything that prevents us from reusing the core?

I think @sameerank is going to get everything into core this week, so we can import it for dynamic evaluation (the workstream after the current one).

Comment thread provider_set_configuration.md Outdated
Comment on lines +132 to +140
> ⚠️ **Two documented formats exist.** The Confluence *PrecomputedConfiguration format*
> page ([5141791092](https://datadoghq.atlassian.net/wiki/spaces/PANA/pages/5141791092/PrecomputedConfiguration+format))
> describes an OpenFeature-aligned shape (`type` + `resolution.flagMetadata.experiment`).
> The sample CDN response and the shipped `openfeature-js-client` instead use the
> `PrecomputedFlag`-style shape above, which matches RN's `FlagCacheEntry`. **This plan
> assumes the `PrecomputedFlag`-style shape.** Since `variationValue` is the **typed** value,
> the decoder maps it to RN's `FlagCacheEntry.value` and derives the string
> `variationValue`/`variationType` that native Android exposure tracking expects. Still worth
> checking with the flags team whether the shape is stable across environments and versions.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could you update the wiki to align it with actual returned type?

Comment thread provider_set_configuration.md
Comment thread provider_set_configuration.md Outdated
Comment on lines +230 to +232
In the offline flow the customer still calls `setEvaluationContext` (needed for context matching
and to hand a context to `trackEvaluation`), so under `fetchPolicy: NEVER` this path records the
context without invoking the native fetch and without overwriting the config-populated cache.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

hm... I'm not familiar with RN implementation. Does native side have any state? if native setEvaluationContext returns a configuration, does it actually set anything?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Native side does save state. I made a note that we should think about whether setConfiguration persists (or doesn't) to disk: 7b8af9c

btthomas and others added 5 commits July 7, 2026 12:18
Name each step branch blake.thomas/FFL-2666-PR{N}, based on the previous
step's branch. PR1 branches off this planning branch so every step inherits
the plan. Add a Branch column to the delivery table.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Oleksii Shmalko <oleksii.shmalko@datadoghq.com>
A config loaded via setConfiguration is in-memory only today. Capture as an
open question whether to offer a persist option for offline last-known-values
across cold starts (wire vs parsed, JS-side store vs native flagsDataStore,
auto-load + fetchPolicy interaction, staleness). Likely a fast-follow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The wire variationType union now includes integer and float, so widen the
FFL-2687 validation to boolean|string|number|integer|float|object and note
that integer/float map to a JS number for value while the original
variationType string is preserved for Android's exposure round-trip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Deliver never-fetch as a dedicated offline OpenFeature provider rather than a
fetchPolicy flag. Its initialize/onContextChange use
FlagsClient.setEvaluationContextWithoutFetching and never hit the CDN. Simpler
core, no online/offline mixing, all-JS (no native changes), and the same
provider extends to future offline dynamic rules. Collapse the delivery to
three stacked PRs (PR2 = core offline API incl. the no-fetch method, PR3 =
OfflineProvider + exports) and update the work breakdown and acceptance.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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