From 4ae262f0876c04319a00868b2e49fefc9ad286cb Mon Sep 17 00:00:00 2001 From: Blake Date: Wed, 22 Jul 2026 15:56:06 -0400 Subject: [PATCH 1/8] docs(flags): add FFL-2837 dynamic offline init plan Planning doc for rules-based (dynamic) offline feature-flag init in the React Native SDK: gaps to bridge in @datadog/flagging-core, explicit implementation steps, test plan, and risks/unknowns. Co-Authored-By: Claude Opus 4.8 (1M context) --- dynamic_offline.plan.md | 361 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 361 insertions(+) create mode 100644 dynamic_offline.plan.md diff --git a/dynamic_offline.plan.md b/dynamic_offline.plan.md new file mode 100644 index 000000000..a2e4fb6b5 --- /dev/null +++ b/dynamic_offline.plan.md @@ -0,0 +1,361 @@ +# FFL-2837 — Dynamic (rules-based) offline init in dd-sdk-reactnative + +**Jira:** [FFL-2837 — Building Blocks API for dynamic offline init in ReactNative SDK](https://datadoghq.atlassian.net/browse/FFL-2837) +**Base branch:** `blake.thomas/FFL-2666` (static/precomputed offline; OfflineProvider + `setConfiguration`) +**Work branch:** `blake.thomas/FFL-2837` (this branch, cut from FFL-2666) +**Upstream reference:** [openfeature-js-client PR #336 — FFL-2753 browser `CoreProvider`](https://github.com/DataDog/openfeature-js-client/pull/336) (`sameerank/FFL-2753-browser-core-provider`) + +### Source docs (drafts — both dated Jun 2026, status "In review", so names/versions may still move) +- `./Portable-Flag-Configuration-RFC.md` (repo root, untracked) — defines the building blocks: `ConfigurationWire`, `configurationFromString/ToString`, `CoreProvider`, fetch fns, hooks. +- `./Offline-Initialization-for-Feature-Flagging.md` (repo root, untracked) — offline recipes built from those blocks; the operation is always `configurationFromString(wire) → provider.setConfiguration(config) → evaluate(...)`. +- [ConfigurationWire (Confluence)](https://datadoghq.atlassian.net/wiki/spaces/PANA/pages/5141725646/ConfigurationWire) — the published wire spec. Its **protobuf/base64** rules encoding is the intended target (see §2.5), even though the current code still uses JSON. + +--- + +## 1. Objective + +Today (FFL-2666) the `DatadogOfflineOpenFeatureProvider` serves a **precomputed** configuration: +a single-subject snapshot carrying the exact context it was computed for. `setContext` on a +mismatched context puts the provider into `ERROR`. + +FFL-2837 adds the **rules-based** (dynamic) offline flow: + +- Customer loads a **rules-based** configuration (Universal Flag Configuration) via `setConfiguration` + — still **no network fetch** (this is the OfflineProvider). +- `setContext` / `onContextChange` **must not fetch**. It re-evaluates the loaded rules against the + new context locally and updates the served values. +- Rules-based config is **context-agnostic**: any context is valid; no "mismatch → ERROR". +- Evaluation logic is **imported from `@datadog/flagging-core`** (`evaluate()` / the rules engine), + not reimplemented in RN. + +Both flows coexist behind the same `DatadogOfflineOpenFeatureProvider`. A wire may carry +`precomputed`, rules, or both (precomputed preferred when its context matches, else rules). + +Per the Offline-Init RFC this is **not** a new SDK mode or a dedicated `offlineInit()` — it is the same +`configurationFromString → setConfiguration` loading path, differing only in the configuration kind. + +--- + +## 2. Current state of `@datadog/flagging-core` + +### Published / consumed today +- RN depends on `@datadog/flagging-core@~2.0.1` (`packages/core/package.json:119`, `yarn.lock` → `2.0.1`). +- **2.0.1 has NO rules support.** It exposes only: + - `FlagsConfiguration = { precomputed?: PrecomputedConfiguration }` (no rules slot), + - `configurationFromString` / `configurationToString` handling **only** the precomputed branch, + - `PrecomputedConfiguration`, `PrecomputedConfigurationResponse`, `PrecomputedFlag` types, + - no `evaluation/` module. + +### In flight in PR #336 (`packages/core`) — NOT yet released +- **`configuration/configuration.ts`** adds + `RulesBasedConfiguration = { response: UniversalFlagConfigurationV1; fetchedAt?; etag? }` and + `FlagsConfiguration.rulesBased?: RulesBasedConfiguration`. +- **`configuration/wire.ts`** — `configurationFromString/ToString` round-trip a `rulesBased` branch + (`response` via `JSON.parse` / `JSON.stringify`). +- **`evaluation/`** rules engine, exported from `evaluation/index.ts`: + - `evaluate(config, type, flagKey, defaultValue, context, logger)` — shared entry point. + - `evaluateRulesBasedConfiguration(...)`, `evaluateForSubject(...)` + - `ufc-v1.ts` — `UniversalFlagConfigurationV1`, `Flag`, `Allocation`, `Split`, `Shard`, `VariantType`, `variantTypeToFlagValueType` + - `rules.ts`, `matchesShard.ts`, `sharders.ts` (targeting + bucketing) + - `errors.ts` (`TargetingKeyMissingError`), `evaluationMetadata.ts` +- **`browser/src/openfeature/core-provider.ts`** — the browser `CoreProvider`, the closest analog to + the RN target. Behaviors to mirror (all confirmed by the RFC §CoreProvider): + - `getConfigurationError()` errors only when there is **no** evaluatable config, or a precomputed + config with mismatched context **and no rules fallback**. With rules present, any context is valid. + - `onContextChange` just stores the new context (no fetch); evaluation reads it live. + - `setConfiguration` emits `Ready` / `ConfigurationChanged` / `Error`. + +### `evaluate()` precedence (PR #336 code == RFC §"Evaluation path selection") +1. `precomputed` present **and** context matches → serve precomputed. +2. Else `rulesBased` present → `evaluateRulesBasedConfiguration(...)` (per-call, per-context). +3. Else `precomputed` present (mismatch, no rules) → `ERROR / INVALID_CONTEXT`. +4. Else → `ERROR / PROVIDER_NOT_READY`. + +Rules evaluation folds `targetingKey` into `subjectAttributes.id` and returns `ResolutionDetails` +(value, variant, `flagMetadata { allocationKey, variationType, doLog }`, reason, errorCode). + +--- + +## 2.5 Wire format: three sources, and the intended encoding + +The rules wire shape is described **inconsistently** across the three references: + +| Source | `version` | rules field name | `response` encoding | +|--------|-----------|------------------|---------------------| +| ConfigurationWire (Confluence) — **intended target** | `1` | `rules` | **protobuf, base64** | +| flagging-core PR #336 `wire.ts` (**current code** — what RN calls today) | `1` | `rulesBased` | JSON string (`JSON.parse`) | +| `Portable-Flag-Configuration-RFC.md` | `2` | `server` | JSON-encoded `ServerConfiguration` | + +**Decided-but-not-yet-implemented: the rules `response` is intended to be protobuf-encoded, base64.** +The ConfigurationWire spec is the target. The current flagging-core code takes a shortcut and +`JSON.parse`s the rules `response` (as does the RFC prose) — that is the **not-yet-migrated** state, +not the destination. Treat protobuf as the plan of record; do **not** design RN around the JSON +shortcut persisting. + +Implications for RN (all deferred until flagging-core lands the protobuf path): +- **Decoding stays in flagging-core, not RN.** RN never parses the wire itself — it delegates to + flagging-core's `configurationFromString`. When flagging-core switches the rules branch from + `JSON.parse` to protobuf-decode, RN inherits it via the version bump with **no RN parsing code**. +- **Watch the transitive dep.** A protobuf runtime (e.g. `protobufjs` or generated decoders) will be + pulled in transitively — folds directly into the bundle-size / Hermes review (G5, R4/R5). +- **The `.proto` schema is a prerequisite** and does not exist in a consumable form yet — track it as + part of G2 below. + +**Residual naming/version churn:** the field name (`rules` vs `rulesBased` vs `server`) and `version` +(1 vs 2) are still in flux across drafts. Pin to the released flagging-core version, re-verify the +exported type/name before wiring RN types (R8), and never hard-code the wire field name in RN. + +--- + +## 3. Gaps in `@datadog/flagging-core` we must bridge + +| # | Gap | Blocking? | Action | +|---|-----|-----------|--------| +| G1 | rules support is unmerged (PR #336) and unpublished | **Yes** | Land PR #336's `packages/core` changes, publish a new `@datadog/flagging-core` (>= 2.1 / 3.0). RN bumps the dep. Until then develop against a linked / `npm pack` build. | +| G2 | Rules `response` protobuf encoding not yet implemented | **Yes (upstream)** | Intended encoding is **protobuf/base64** (Confluence); current code `JSON.parse`s it. flagging-core must (a) publish the `.proto` schema and (b) switch its rules branch to protobuf-decode. RN needs **no parser** (delegates to `configurationFromString`) but inherits the transitive protobuf runtime → feeds G5/R4/R5. Until then, dev/tests run against the interim JSON shape. | +| G3 | Rules engine / UFC types exported from package root | **Yes** | Confirm `evaluate`, `UniversalFlagConfigurationV1`, `RulesBasedConfiguration` are re-exported (`core/src/index.ts` re-exports `./evaluation` and `./configuration`). Verify after publish. | +| G4 | Exposure/telemetry parity for the rules path | Partial | `evaluate()` returns only `ResolutionDetails`; it does **not** call RN native exposure tracking. RN synthesizes the fields `trackEvaluation` needs (see §4 step 5). Confirm `doLog` gating (allocation-level) with flagging-core owners. | +| G5 | Bundle size / RN + Hermes compat of the engine | **Medium** | RFC flags the rules evaluator as materially larger than precomputed lookup and proposes split entrypoints (`.../precomputed` vs full `CoreProvider`). See §4 step 6 (bundle decision) and R5. Verify `sharders.ts` hashing runs under Hermes (no Node `crypto`/browser-only APIs). | +| G6 | Obfuscated / hashed rules payloads | Low/Medium | RFC leaves "how obfuscation fits in" **open**. Base64 is not a security control; hashed values must still work for equality / `ONE_OF` checks. Confirm whether client rules can be obfuscated and that the engine handles/reject it. | + +--- + +## 4. Implementation steps in dd-sdk-reactnative + +Architecture we inherit: +- `DatadogOfflineOpenFeatureProvider` (react-native-openfeature) — OpenFeature lifecycle → maps to + `FlagsClient` reconcile results + provider events. +- `FlagsClient` (core) — owns `loadedConfiguration`, `reconcile()`, served cache, evaluation. +- `configuration/` (core) — wire parse + decode + context helpers. + +The precomputed path **precomputes a `Map`** at load and serves from it. +The rules path is **lazy**: it cannot precompute all flags, so it evaluates per `getX` call against +the live context via flagging-core `evaluate()`. + +### Step 0 — Prereqs (§3) +- [ ] PR #336 core changes merged; `@datadog/flagging-core` published with rules support. +- [ ] Bump `@datadog/flagging-core` in `packages/core/package.json` **and** + `packages/react-native-openfeature/package.json`; update `yarn.lock`. +- [ ] Re-verify the published rules field name / version and the exported symbol names (R8). + +### Step 1 — Parsed-config type surface (`core/src/flags/configuration/types.ts`) +- [ ] Re-export the rules types from flagging-core alongside the precomputed ones: + ```ts + import type { + FlagsConfiguration, + PrecomputedConfiguration, + RulesBasedConfiguration, // NEW + UniversalFlagConfigurationV1, // NEW + // ...existing precomputed types + } from '@datadog/flagging-core'; + + export type ParsedRulesBasedConfiguration = RulesBasedConfiguration; // NEW + export type ParsedUniversalFlagConfiguration = UniversalFlagConfigurationV1; // NEW + // ParsedFlagsConfiguration = FlagsConfiguration already gains `rulesBased` once bumped. + ``` +- [ ] Re-export from `flags/configuration/index.ts` and package entry `core/src/index.tsx` as needed. + +### Step 2 — Wire parsing (`core/src/flags/configuration/wire.ts`) +- [ ] **No RN code change expected**: RN re-exports `configurationFromString`/`configurationToString` + from flagging-core; the bumped version handles the rules branch automatically — including the future + switch from the interim JSON shape to **protobuf/base64** decode (§2.5), which RN inherits with no + parsing code of its own. +- [ ] Add an RN test asserting a rules wire round-trips through the re-export (guards the bump + the + field-name churn in R8). Use whatever encoding the pinned flagging-core version emits (JSON interim, + protobuf once landed). + +### Step 3 — `LoadedConfigurationState` + `loadConfiguration` (`core/src/flags/FlagsClient.ts`) +`loadConfiguration` already carries a **"FORWARD-COMPAT SEAM"** comment (FlagsClient.ts:247-255) saying +rules must be handled **before** the precomputed guard. Implement it: +- [ ] Add a variant to `LoadedConfigurationState` (no decoded `Map` — rules are lazy): + ```ts + | { kind: 'rulesBased'; configuration: ParsedRulesBasedConfiguration } + ``` +- [ ] In `loadConfiguration`, **before** the `!precomputed` guard: + ```ts + if (configuration?.rulesBased) { + // Optionally validate the UFC envelope (flags is an object, etc.). + return { kind: 'rulesBased', configuration: configuration.rulesBased }; + } + ``` +- [ ] Decide precedence when **both** present (mirror `evaluate()` / RFC): precomputed-if-context-matches + wins, else rules. Simplest faithful port: keep the whole `FlagsConfiguration` on the state (or a + `both` kind) and let `evaluate()` arbitrate (§Step 6). + +### Step 4 — `reconcile()` for rules (`core/src/flags/FlagsClient.ts`) +- [ ] `kind === 'rulesBased'`: **any** external context is valid (no `INVALID_CONTEXT`). Set + `evaluationContext` to the external override or an empty `{ targetingKey: '', attributes: {} }` when + none is set. `configurationStatus = 'ready'`. Do **not** populate `flagsCache` (lazy). +- [ ] Leave the precomputed branch unchanged. If storing both, the mismatch→error branch fires **only** + when there is no rules fallback (mirror browser `getConfigurationError`). + +### Step 5 — Evaluation path (`core/src/flags/FlagsClient.ts` `getDetails`) +Largest change. `getDetails` currently serves from `flagsCache` (precomputed/online). +- [ ] Branch on loaded kind. For rules, call flagging-core `evaluate()`: + ```ts + import { evaluate } from '@datadog/flagging-core'; + const details = evaluate(flagsConfig, type, key, defaultValue, toOFContext(this.evaluationContext), logger); + ``` + Note: `evaluate` takes an **OpenFeature-flat** context (`{ targetingKey, ...attrs }`), while + `FlagsClient` holds `{ targetingKey, attributes }`. Add an adapter (inverse of `toDdContext` / + `normalizeWireContext`); flagging-core folds `targetingKey → subjectAttributes.id` internally. +- [ ] Map flagging-core `ResolutionDetails` → RN `FlagDetails` (value, variant, `allocationKey` from + `flagMetadata`, reason, errorCode/errorMessage). Let `evaluate` produce `TYPE_MISMATCH`/`FLAG_NOT_FOUND`. +- [ ] **Exposure tracking:** rules `evaluate()` has no tracking side effect. Synthesize a + `FlagCacheEntry` from the resolution (`key`, `value`, `variationKey`=variant, `allocationKey`, + `variationType` from `flagMetadata`, `variationValue`=stringified value, `reason`, `doLog`, + `extraLogging`) and call `this.track(entry, this.evaluationContext)` — **gate on `doLog`** (confirm + policy: the precomputed path currently tracks unconditionally; decide match-vs-fix — G4). +- [ ] Keep precomputed + online paths serving from `flagsCache` untouched. + +### Step 6 — Precedence when both present + bundle-size decision +- [ ] Implement the flagging-core order faithfully (precomputed-if-match → rules). Easiest: pass the + full `FlagsConfiguration` into `evaluate()` and let it arbitrate; or route **everything** through + `evaluate()` for one code path (bigger refactor — weigh against the native-exposure parity the + current `Map`+`track` path guarantees). +- [ ] **Bundle-size decision (RFC tree-shaking concern + Offline-Init open Q "max wire size"):** a + static `import { evaluate } from '@datadog/flagging-core'` pulls the whole rules engine (sharders, + rules, ufc) into **every** RN app, including precomputed-only users. Options: + 1. Accept the cost (simplest; measure it). + 2. **Dynamic `import()`** of the rules evaluator only when a rules config is loaded (lazy; keeps the + precomputed-only bundle lean — matches the RFC's split-entrypoint intent). + 3. Ask flagging-core for a subpath export (`@datadog/flagging-core/evaluation`) so RN imports only + the engine when needed. + Pick one; record the measured delta. (R5) + +### Step 7 — `DatadogOfflineOpenFeatureProvider` (`react-native-openfeature/src/offlineProvider.ts`) +- [ ] `initialize` / `onContextChange` already do not fetch. For rules, `applyContext`'s reconcile + returns `ready` for any context, so the mismatch-throw path disappears naturally. Verify the + empty-context re-adopt logic still reads sensibly for rules (an empty context for rules is just an + anonymous subject; there is no embedded context to re-adopt). +- [ ] **Update the class doc comment** — it currently states precomputed-only semantics ("you should + **not** call `OpenFeature.setContext`"). For rules, `setContext` **is** the intended dynamic path. +- [ ] `setConfiguration` event mapping (`Ready` / `ConfigurationChanged` / `Error`) is already generic + and matches the RFC event model; confirm a rules `ready` triggers the right transitions. +- [ ] **Opt-in / security posture (both RFCs):** client-side rules evaluation exposes targeting logic + and is a security-sensitive, explicitly opt-in path. Decide whether the rules-based offline provider + is gated behind an explicit opt-in flag/import and documented as such. (R7) + +### Step 8 — Exports, examples & docs +- [ ] Export new public types (`ParsedRulesBasedConfiguration`) from + `react-native-openfeature/src/index.ts` and `core/src/index.tsx` if customers need them. +- [ ] Update package READMEs + example apps (`example/src/flags/flagsProvider.ts`, + `example-new-architecture/flags/flagsProvider.ts`) with a rules-based offline snippet + (`configurationFromString(wire) → setConfiguration → setContext(a) / setContext(b)`). + +--- + +## 5. All imports added to dd-sdk-reactnative + +From `@datadog/flagging-core` (post-bump): +- `evaluate` — value import in `core/src/flags/FlagsClient.ts` (or behind a dynamic `import()` per §Step 6). +- `type RulesBasedConfiguration`, `type UniversalFlagConfigurationV1`, `type FlagsConfiguration`, + `type FlagTypeToValue` — in `configuration/types.ts` and `FlagsClient.ts`. +- (already imported) `configurationFromString`, `configurationToString`, precomputed types. +- Possibly `type ResolutionDetails`, `type Logger` if threaded (or keep using `@openfeature/web-sdk`). + +Internal (RN) new: +- A context adapter (RN `{targetingKey, attributes}` ↔ OpenFeature-flat `{targetingKey, ...attrs}`), + likely in `flags/configuration/context.ts` or `flags/internal.ts`. +- `ParsedRulesBasedConfiguration` type export chain (types.ts → configuration/index.ts → index.tsx). + +**No new native surface required** — `NativeDdFlags.trackEvaluation(clientName, key, rawFlag, +targetingKey, attributes)` already accepts a synthesized flag object, and rules evaluation is entirely JS. + +--- + +## 6. Test plan + +Model on existing suites: `react-native-openfeature/src/__tests__/offlineProvider.test.ts`, +`offlineProvider.integration.test.ts`, `core/src/flags/__tests__/FlagsClient.test.ts`, +`core/src/flags/configuration/__tests__/wire.test.ts`. + +### Wire / config (core) +- [ ] `configurationFromString` parses a rules wire into `{ rulesBased: { response: UFC } }`. +- [ ] Round-trip `configurationToString(configurationFromString(wire))`. +- [ ] Malformed / unsupported-version wire → empty config (lenient) → classified `GENERAL` on load. +- [ ] Wire carrying **both** precomputed and rules parses both. +- [ ] Guard test that fails loudly if the published rules field name/version differs from expectations (R8). + +### FlagsClient (core) — rules load & reconcile +- [ ] `setConfiguration` rules-only → `ready` for an **empty** context. +- [ ] `setEvaluationContextWithoutFetching(anyContext)` on rules → `ready` (never `INVALID_CONTEXT`); **no** native fetch. +- [ ] `resetEvaluationContextWithoutFetching()` on rules → `ready`. +- [ ] No config loaded → `PROVIDER_NOT_READY` (regression guard). +- [ ] Both precomputed + rules: matching context serves precomputed; mismatch falls back to rules (not + `ERROR`); precomputed-only mismatch still `INVALID_CONTEXT`. + +### FlagsClient (core) — rules evaluation +- [ ] `getBooleanValue/String/Number/Object` return the rules-evaluated value for a targeting key that + matches an allocation; return the else/fallthrough variant otherwise. +- [ ] **Dynamic behavior:** changing context between two evaluations yields **different** values when + the two subjects bucket differently (the core point of FFL-2837). +- [ ] Flag-not-found → `FLAG_NOT_FOUND` + default. Type mismatch → `TYPE_MISMATCH` + default. +- [ ] Missing targeting key where a rule requires it → `TARGETING_KEY_MISSING`/`INVALID_CONTEXT` + default. +- [ ] Exposure: `native.trackEvaluation` called with a correctly synthesized flag (`variationKey`, + `allocationKey`, `variationValue` string) when `doLog` true; not called when false (per agreed policy). + +### Provider (react-native-openfeature) +- [ ] Rules config loaded before registration → provider reaches `READY` (no context needed). +- [ ] `setContext(ctxA)` then `setContext(ctxB)` re-evaluates locally, **no fetch**, emits expected + lifecycle (RECONCILING→READY), values reflect each context. +- [ ] `setConfiguration` valid rules → `CONFIGURATION_CHANGED` (and `READY` if recovering from error). +- [ ] `setConfiguration` empty/invalid → `PROVIDER_ERROR` with a top-level errorCode. +- [ ] Determinism: same (context, config) → same bucketed variant across calls. +- [ ] Regression: all existing precomputed offline tests still pass. + +### Integration / non-functional +- [ ] End-to-end: parse a real rules `ConfigurationWire` sample → set provider → evaluate several flags + across several contexts; assert values + exposure calls. Use a UFC fixture from ffe-service or the + flagging-core test fixtures. +- [ ] **Bundle-size check** (R5): measure the delta the rules engine adds; confirm the precomputed-only + path does not regress (validates the §Step 6 decision). +- [ ] **Hermes smoke test**: rules evaluation (incl. `sharders` hashing) runs under Hermes. + +--- + +## 7. Risks & unknowns + +1. **flagging-core rules support is unmerged/unpublished (G1, G3).** Everything blocks on PR #336 + landing + a release. Mitigate: develop against a linked / `npm pack` build; keep the RN diff + isolated so the dependency bump is the only integration point. +2. **Two evaluation paths in `FlagsClient`.** Precomputed serves from a decoded `Map`; rules evaluate + lazily via `evaluate()`. Risk of divergent behavior (reason codes, exposure, type checks). Consider + routing precomputed through `evaluate()` too for one path — weigh against the native-exposure parity + the current `Map`+`track` path guarantees. +3. **Exposure parity (G4).** `evaluate()` has no tracking side effect; RN synthesizes the flag object + for `trackEvaluation`. `doLog` gating (allocation-level) and RUM correlation must match the online + path. Confirm with flagging-core / ffe owners. +4. **Bundle size / tree-shaking (G5).** The RFC explicitly calls the rules evaluator larger than the + precomputed lookup and proposes split entrypoints. A naive static import taxes every RN app. Decide + accept-cost vs. dynamic `import()` vs. request a subpath export; measure. See §Step 6. +5. **Hermes/RN bundling of the engine.** `sharders.ts` hashing may assume Node/browser APIs. Verify it + runs under Hermes; add the smoke test above. +6. **Context shape adapters.** flagging-core wants OpenFeature-flat context and folds `targetingKey→id`; + RN holds `{targetingKey, attributes}`. Normalization mismatches could mis-bucket. Cover with the + determinism tests. +7. **Security / opt-in (both RFCs).** Client-side rules expose targeting logic and are an explicitly + opt-in, security-sensitive path. Decide whether to gate the rules offline provider behind an opt-in + and document it. Public client credentials must not fetch private rules by default (fetch is out of + scope here, but the posture matters for docs). +8. **Wire naming/version churn (§2.5).** `rulesBased` (code) vs `server` (RFC) vs `rules` (Confluence); + `version` 1 vs 2. The docs are drafts. Pin to the released flagging-core version, never hard-code the + field name in RN, and add a guard test. +9. **Obfuscated / hashed UFC (G6).** RFC leaves obfuscation open. Confirm whether client rules can be + obfuscated and that the engine handles hashed equality / `ONE_OF` — else reject predictably like the + precomputed path does. + +--- + +## 8. Open questions to resolve before coding +- [ ] Which published `@datadog/flagging-core` version carries rules, and does its package root export + `evaluate`, `UniversalFlagConfigurationV1`, `RulesBasedConfiguration`? What is the final rules field + name/version on the wire? (G1/G3/R8) +- [ ] Protobuf rules encoding: when will flagging-core publish the `.proto` schema and switch its rules + `response` from JSON to protobuf/base64-decode? What protobuf runtime does it pull in, and is it + Hermes-safe? (G2/§2.5) +- [ ] Exposure/`doLog` policy for rules — match precomputed's unconditional `track`, or gate on `doLog`? (G4) +- [ ] Do we route precomputed through `evaluate()` too, or keep two paths? (R2) +- [ ] Bundle-size approach: accept, dynamic-import, or subpath export? (R4/§Step 6) +- [ ] Is client-side rules an explicit opt-in in RN, and how is it surfaced/documented? (R7) +- [ ] Can rules payloads be obfuscated for mobile, and must the engine handle hashed values? (G6) From 0335945a80732d4d4acc6b7cca8a145af72e5dbd Mon Sep 17 00:00:00 2001 From: Blake Date: Wed, 22 Jul 2026 16:58:50 -0400 Subject: [PATCH 2/8] docs(flags): record FFL-2837 design decisions (D3-D7), punt Q1-Q2 Resolve five of the plan's open questions: gate rules exposure on doLog (D3), keep two evaluation paths (D4), accept static-import bundle cost and reject dynamic import (D5), no offline opt-in gate for rules (D6), treat obfuscation/hashing as upstream (D7). Punt the flagging-core version/protobuf questions (Q1-Q2) to coordinate with upstream owners. Co-Authored-By: Claude Opus 4.8 (1M context) --- dynamic_offline.plan.md | 128 +++++++++++++++++++++++++--------------- 1 file changed, 79 insertions(+), 49 deletions(-) diff --git a/dynamic_offline.plan.md b/dynamic_offline.plan.md index a2e4fb6b5..1a7e42d71 100644 --- a/dynamic_offline.plan.md +++ b/dynamic_offline.plan.md @@ -116,7 +116,7 @@ exported type/name before wiring RN types (R8), and never hard-code the wire fie | G3 | Rules engine / UFC types exported from package root | **Yes** | Confirm `evaluate`, `UniversalFlagConfigurationV1`, `RulesBasedConfiguration` are re-exported (`core/src/index.ts` re-exports `./evaluation` and `./configuration`). Verify after publish. | | G4 | Exposure/telemetry parity for the rules path | Partial | `evaluate()` returns only `ResolutionDetails`; it does **not** call RN native exposure tracking. RN synthesizes the fields `trackEvaluation` needs (see §4 step 5). Confirm `doLog` gating (allocation-level) with flagging-core owners. | | G5 | Bundle size / RN + Hermes compat of the engine | **Medium** | RFC flags the rules evaluator as materially larger than precomputed lookup and proposes split entrypoints (`.../precomputed` vs full `CoreProvider`). See §4 step 6 (bundle decision) and R5. Verify `sharders.ts` hashing runs under Hermes (no Node `crypto`/browser-only APIs). | -| G6 | Obfuscated / hashed rules payloads | Low/Medium | RFC leaves "how obfuscation fits in" **open**. Base64 is not a security control; hashed values must still work for equality / `ONE_OF` checks. Confirm whether client rules can be obfuscated and that the engine handles/reject it. | +| G6 | Obfuscated / hashed rules payloads | **No (D7)** | Obfuscation is an **upstream (flagging-core) concern**. If a customer enables it, they pre-hash their evaluation context with the same function used to hash the rules, and the engine does hashed equality / `ONE_OF` comparisons. **RN needs no special handling and no rejection path** — assume it works; just ensure RN's context normalization passes string values through untouched (test it). | --- @@ -201,27 +201,36 @@ Largest change. `getDetails` currently serves from `flagsCache` (precomputed/onl `normalizeWireContext`); flagging-core folds `targetingKey → subjectAttributes.id` internally. - [ ] Map flagging-core `ResolutionDetails` → RN `FlagDetails` (value, variant, `allocationKey` from `flagMetadata`, reason, errorCode/errorMessage). Let `evaluate` produce `TYPE_MISMATCH`/`FLAG_NOT_FOUND`. -- [ ] **Exposure tracking:** rules `evaluate()` has no tracking side effect. Synthesize a +- [ ] **Exposure tracking (DECIDED — D3):** rules `evaluate()` has no tracking side effect. Synthesize a `FlagCacheEntry` from the resolution (`key`, `value`, `variationKey`=variant, `allocationKey`, `variationType` from `flagMetadata`, `variationValue`=stringified value, `reason`, `doLog`, - `extraLogging`) and call `this.track(entry, this.evaluationContext)` — **gate on `doLog`** (confirm - policy: the precomputed path currently tracks unconditionally; decide match-vs-fix — G4). + `extraLogging`) and call `this.track(entry, this.evaluationContext)` **only when `doLog` is true**. + Separately, confirm whether the precomputed path's *unconditional* `track` is correct (native fetch + may already pre-filter to loggable flags) or a pre-existing bug to align — track as a follow-up, do + **not** silently change precomputed behavior here. - [ ] Keep precomputed + online paths serving from `flagsCache` untouched. ### Step 6 — Precedence when both present + bundle-size decision -- [ ] Implement the flagging-core order faithfully (precomputed-if-match → rules). Easiest: pass the - full `FlagsConfiguration` into `evaluate()` and let it arbitrate; or route **everything** through - `evaluate()` for one code path (bigger refactor — weigh against the native-exposure parity the - current `Map`+`track` path guarantees). -- [ ] **Bundle-size decision (RFC tree-shaking concern + Offline-Init open Q "max wire size"):** a - static `import { evaluate } from '@datadog/flagging-core'` pulls the whole rules engine (sharders, - rules, ufc) into **every** RN app, including precomputed-only users. Options: - 1. Accept the cost (simplest; measure it). - 2. **Dynamic `import()`** of the rules evaluator only when a rules config is loaded (lazy; keeps the - precomputed-only bundle lean — matches the RFC's split-entrypoint intent). - 3. Ask flagging-core for a subpath export (`@datadog/flagging-core/evaluation`) so RN imports only - the engine when needed. - Pick one; record the measured delta. (R5) +- [ ] **Two paths (DECIDED — D4):** keep precomputed on the decoded `Map` + `track`; only the rules + branch calls `evaluate()`. Do **not** route precomputed through `evaluate()`. Factor the shared + `ResolutionDetails → FlagDetails` + synthesize-`FlagCacheEntry`-for-`track` mapping into one helper so + both paths reuse it. Rationale: no risk to the shipped FFL-2666 precomputed flow, keeps the O(1) + precomputed lookup, and keeps the rules engine **out** of the precomputed-only path (serves D5). + Implement the both-present order (precomputed-if-context-matches → rules) by passing the full + `FlagsConfiguration` into `evaluate()` from the rules branch and letting it arbitrate. +- [ ] **Bundle size (DECIDED — D5): accept the static import cost for MVP, measure the delta, and if it + is unacceptable do a provider split + subpath export — NOT dynamic `import()`.** Key constraint: + Metro does not do real code-splitting for standard RN app builds, so a dynamic `import()` still ships + the engine in the bundle (it only defers module *init* via `inlineRequires`) — it does **not** reduce + size on RN. So: + 1. MVP: `import { evaluate } from '@datadog/flagging-core'` statically; measure the added bytes + (engine + eventual protobuf runtime). + 2. Only if the delta is unacceptable: ship a precomputed-only provider + (`DatadogPrecomputedOfflineProvider`) that imports a flagging-core `@datadog/flagging-core/precomputed` + subpath, alongside the full rules-capable offline provider — mirrors the RFC's + `PrecomputedCoreProvider` vs `CoreProvider`. This is the *only* option that actually removes the + engine from precomputed-only bundles on RN. (This split is a **bundle-size lever only** — it is not + a security gate; see D6.) ### Step 7 — `DatadogOfflineOpenFeatureProvider` (`react-native-openfeature/src/offlineProvider.ts`) - [ ] `initialize` / `onContextChange` already do not fetch. For rules, `applyContext`'s reconcile @@ -232,9 +241,14 @@ Largest change. `getDetails` currently serves from `flagsCache` (precomputed/onl **not** call `OpenFeature.setContext`"). For rules, `setContext` **is** the intended dynamic path. - [ ] `setConfiguration` event mapping (`Ready` / `ConfigurationChanged` / `Error`) is already generic and matches the RFC event model; confirm a rules `ready` triggers the right transitions. -- [ ] **Opt-in / security posture (both RFCs):** client-side rules evaluation exposes targeting logic - and is a security-sensitive, explicitly opt-in path. Decide whether the rules-based offline provider - is gated behind an explicit opt-in flag/import and documented as such. (R7) +- [ ] **No opt-in gate for offline rules (DECIDED — D6).** Online/offline is orthogonal to + precomputed/rules. In the offline flow the customer already holds the rules wire and explicitly loads + it via `setConfiguration`, so there is no hidden behavior to gate — loading a rules wire into the + OfflineProvider just works. The real security control lives at **fetch time** (a public client token + must not be scoped to pull private rules); that is enforced server-side and belongs to the + **OnlineProvider** rules-fetch work — **out of scope for FFL-2837**. Only obligation here: a docs + caveat that rules ship on-device and are reverse-engineerable (the customer's informed choice; base64 + is not a security control). The precomputed-only provider from D5, if built, is a bundle lever only. ### Step 8 — Exports, examples & docs - [ ] Export new public types (`ParsedRulesBasedConfiguration`) from @@ -248,7 +262,8 @@ Largest change. `getDetails` currently serves from `flagsCache` (precomputed/onl ## 5. All imports added to dd-sdk-reactnative From `@datadog/flagging-core` (post-bump): -- `evaluate` — value import in `core/src/flags/FlagsClient.ts` (or behind a dynamic `import()` per §Step 6). +- `evaluate` — **static** value import in `core/src/flags/FlagsClient.ts` (D5: no dynamic import — it + does not reduce bundle size on Metro). - `type RulesBasedConfiguration`, `type UniversalFlagConfigurationV1`, `type FlagsConfiguration`, `type FlagTypeToValue` — in `configuration/types.ts` and `FlagsClient.ts`. - (already imported) `configurationFromString`, `configurationToString`, precomputed types. @@ -292,8 +307,11 @@ Model on existing suites: `react-native-openfeature/src/__tests__/offlineProvide the two subjects bucket differently (the core point of FFL-2837). - [ ] Flag-not-found → `FLAG_NOT_FOUND` + default. Type mismatch → `TYPE_MISMATCH` + default. - [ ] Missing targeting key where a rule requires it → `TARGETING_KEY_MISSING`/`INVALID_CONTEXT` + default. -- [ ] Exposure: `native.trackEvaluation` called with a correctly synthesized flag (`variationKey`, - `allocationKey`, `variationValue` string) when `doLog` true; not called when false (per agreed policy). +- [ ] Exposure (D3): `native.trackEvaluation` called with a correctly synthesized flag (`variationKey`, + `allocationKey`, `variationValue` string) **when `doLog` is true; not called when `doLog` is false**. +- [ ] Hashed/obfuscated (D7): a context with string attribute values (as a customer would supply + pre-hashed) survives `processEvaluationContext`/the OpenFeature-flat adapter **unmodified**, so + hashed equality / `ONE_OF` matching in the engine can work. ### Provider (react-native-openfeature) - [ ] Rules config loaded before registration → provider reaches `READY` (no context needed). @@ -319,43 +337,55 @@ Model on existing suites: `react-native-openfeature/src/__tests__/offlineProvide 1. **flagging-core rules support is unmerged/unpublished (G1, G3).** Everything blocks on PR #336 landing + a release. Mitigate: develop against a linked / `npm pack` build; keep the RN diff isolated so the dependency bump is the only integration point. -2. **Two evaluation paths in `FlagsClient`.** Precomputed serves from a decoded `Map`; rules evaluate - lazily via `evaluate()`. Risk of divergent behavior (reason codes, exposure, type checks). Consider - routing precomputed through `evaluate()` too for one path — weigh against the native-exposure parity - the current `Map`+`track` path guarantees. +2. **Two evaluation paths in `FlagsClient` (DECIDED D4 — keep two).** Precomputed serves from a decoded + `Map`; rules evaluate lazily via `evaluate()`. Residual risk: divergent reason codes / type checks + between the two — mitigated by the shared `ResolutionDetails → FlagDetails` mapping helper and the + regression tests. 3. **Exposure parity (G4).** `evaluate()` has no tracking side effect; RN synthesizes the flag object for `trackEvaluation`. `doLog` gating (allocation-level) and RUM correlation must match the online path. Confirm with flagging-core / ffe owners. -4. **Bundle size / tree-shaking (G5).** The RFC explicitly calls the rules evaluator larger than the - precomputed lookup and proposes split entrypoints. A naive static import taxes every RN app. Decide - accept-cost vs. dynamic `import()` vs. request a subpath export; measure. See §Step 6. +4. **Bundle size / tree-shaking (DECIDED D5 — accept + measure).** The rules engine (and eventual + protobuf runtime) ships in every app that imports the offline provider; Metro won't code-split it + away. Residual risk: the measured delta is unacceptable for precomputed-only users — fallback is the + provider-split + subpath export (not dynamic import). See §Step 6. 5. **Hermes/RN bundling of the engine.** `sharders.ts` hashing may assume Node/browser APIs. Verify it runs under Hermes; add the smoke test above. 6. **Context shape adapters.** flagging-core wants OpenFeature-flat context and folds `targetingKey→id`; RN holds `{targetingKey, attributes}`. Normalization mismatches could mis-bucket. Cover with the determinism tests. -7. **Security / opt-in (both RFCs).** Client-side rules expose targeting logic and are an explicitly - opt-in, security-sensitive path. Decide whether to gate the rules offline provider behind an opt-in - and document it. Public client credentials must not fetch private rules by default (fetch is out of - scope here, but the posture matters for docs). +7. **Security posture (DECIDED D6 — no offline opt-in gate).** Offline rules are customer-supplied, so + there is nothing to gate; the real control is fetch-time API-token scoping in the OnlineProvider + (out of scope for FFL-2837). Residual: a docs caveat that rules are reverse-engineerable on-device. 8. **Wire naming/version churn (§2.5).** `rulesBased` (code) vs `server` (RFC) vs `rules` (Confluence); `version` 1 vs 2. The docs are drafts. Pin to the released flagging-core version, never hard-code the field name in RN, and add a guard test. -9. **Obfuscated / hashed UFC (G6).** RFC leaves obfuscation open. Confirm whether client rules can be - obfuscated and that the engine handles hashed equality / `ONE_OF` — else reject predictably like the - precomputed path does. +9. **Obfuscated / hashed UFC (DECIDED D7 — upstream, assume it works).** Hashing is a flagging-core + engine concern; the customer hashes their context with the same function. RN adds no handling and no + rejection — only a test that context normalization preserves (hashed) string values. --- -## 8. Open questions to resolve before coding -- [ ] Which published `@datadog/flagging-core` version carries rules, and does its package root export - `evaluate`, `UniversalFlagConfigurationV1`, `RulesBasedConfiguration`? What is the final rules field - name/version on the wire? (G1/G3/R8) -- [ ] Protobuf rules encoding: when will flagging-core publish the `.proto` schema and switch its rules - `response` from JSON to protobuf/base64-decode? What protobuf runtime does it pull in, and is it - Hermes-safe? (G2/§2.5) -- [ ] Exposure/`doLog` policy for rules — match precomputed's unconditional `track`, or gate on `doLog`? (G4) -- [ ] Do we route precomputed through `evaluate()` too, or keep two paths? (R2) -- [ ] Bundle-size approach: accept, dynamic-import, or subpath export? (R4/§Step 6) -- [ ] Is client-side rules an explicit opt-in in RN, and how is it surfaced/documented? (R7) -- [ ] Can rules payloads be obfuscated for mobile, and must the engine handle hashed values? (G6) +## 8. Decisions & remaining open questions + +### Decisions (2026-07-22) +- **D3 — Exposure/`doLog`:** rules-path exposure is **gated on `doLog`** (track only when true). Precomputed's + current unconditional `track` is left as-is pending confirmation it isn't a pre-existing bug (follow-up). (§Step 5, R3) +- **D4 — Evaluation paths:** **keep two paths** — precomputed on the decoded `Map` + `track`, rules via + `evaluate()` — with a shared `ResolutionDetails → FlagDetails` mapping helper. No unification. (§Step 6, R2) +- **D5 — Bundle size:** **accept the static-import cost for MVP and measure it.** Dynamic `import()` is + rejected (Metro doesn't code-split, so it wouldn't shrink the bundle). If the delta is unacceptable, + fall back to a precomputed-only provider + `@datadog/flagging-core/precomputed` subpath. (§Step 6, R4) +- **D6 — Security/opt-in:** **no opt-in gate for offline rules** — offline is orthogonal to rules, and the + wire is customer-supplied. The real control is fetch-time API-token scoping in the OnlineProvider + (out of scope). Only a docs caveat is owed. The D5 provider split, if built, is a bundle lever, not a + security gate. (§Step 7, R7) +- **D7 — Obfuscation/hashing:** **upstream concern; assume it works.** Customer pre-hashes context with the + same function used on the rules; the engine does hashed comparisons. RN adds no handling/rejection, only + a normalization-preserves-strings test. (G6, R9) + +### Remaining open questions (PUNTED — revisit with flagging-core owners; do not block planning) +- [ ] **Q1 (punted):** which published `@datadog/flagging-core` version carries rules; confirm root exports + (`evaluate`, `UniversalFlagConfigurationV1`, `RulesBasedConfiguration`) and the final wire field + name/version. (G1/G3/R8) +- [ ] **Q2 (punted):** protobuf rules encoding — when flagging-core publishes the `.proto` and switches + `response` from JSON to protobuf/base64-decode; which protobuf runtime, and is it Hermes-safe? (G2/§2.5) From 1bea6ab830a7cac2c15fdf332047735751e72c26 Mon Sep 17 00:00:00 2001 From: Blake Date: Wed, 22 Jul 2026 17:13:15 -0400 Subject: [PATCH 3/8] docs(flags): link RFCs to canonical Google Docs Point the Portable Flag Configuration and Offline Initialization references at their source Google Docs, keeping the local snapshot filenames noted alongside. Co-Authored-By: Claude Opus 4.8 (1M context) --- dynamic_offline.plan.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dynamic_offline.plan.md b/dynamic_offline.plan.md index 1a7e42d71..e0ca48e2c 100644 --- a/dynamic_offline.plan.md +++ b/dynamic_offline.plan.md @@ -6,8 +6,8 @@ **Upstream reference:** [openfeature-js-client PR #336 — FFL-2753 browser `CoreProvider`](https://github.com/DataDog/openfeature-js-client/pull/336) (`sameerank/FFL-2753-browser-core-provider`) ### Source docs (drafts — both dated Jun 2026, status "In review", so names/versions may still move) -- `./Portable-Flag-Configuration-RFC.md` (repo root, untracked) — defines the building blocks: `ConfigurationWire`, `configurationFromString/ToString`, `CoreProvider`, fetch fns, hooks. -- `./Offline-Initialization-for-Feature-Flagging.md` (repo root, untracked) — offline recipes built from those blocks; the operation is always `configurationFromString(wire) → provider.setConfiguration(config) → evaluate(...)`. +- [Portable Flag Configuration RFC](https://docs.google.com/document/d/1OWNBtXtSk535VXqf-9fqsAmU9W8kpFLAwxYi2y1qyQQ/edit?pli=1&tab=t.0#heading=h.n52036mkzewg) (local snapshot: `./Portable-Flag-Configuration-RFC.md`, repo root, untracked) — defines the building blocks: `ConfigurationWire`, `configurationFromString/ToString`, `CoreProvider`, fetch fns, hooks. +- [Offline Initialization for Feature Flagging RFC](https://docs.google.com/document/d/1q1GlEbAgCGuO1OWfGbmKQkk5Oo-rE7YQwq29kMJJ4II/edit?pli=1&tab=t.0#heading=h.rnd972k0hiyer) (local snapshot: `./Offline-Initialization-for-Feature-Flagging.md`, repo root, untracked) — offline recipes built from those blocks; the operation is always `configurationFromString(wire) → provider.setConfiguration(config) → evaluate(...)`. - [ConfigurationWire (Confluence)](https://datadoghq.atlassian.net/wiki/spaces/PANA/pages/5141725646/ConfigurationWire) — the published wire spec. Its **protobuf/base64** rules encoding is the intended target (see §2.5), even though the current code still uses JSON. --- From 6508f764c6f7f36667d0b891752970ca78e4f161 Mon Sep 17 00:00:00 2001 From: Blake Date: Thu, 23 Jul 2026 09:42:00 -0400 Subject: [PATCH 4/8] docs(flags): incorporate review rounds 5-6 + obfuscation RFC Fold in the Obfuscation RFC (salted ONE_OF_SHA256 operators + binary structure) and the round-5/6 review findings. Obfuscation operators are absent from 2.0.1 and today silently fall back to DEFAULT, so unknown operators must be rejected as GENERAL, derived from the pinned OperatorType rather than an RN-maintained set. Sync SHA-256 is new bundle mass (Hermes+JSC). The hash protocol is unspecified and needs cross-SDK vectors plus malformed-condition load validation. Unsupported operators invalidate the rules branch only, keeping a valid precomputed sibling. Corrected the threat model and the full "what stays visible" UFC list. Co-Authored-By: Claude Opus 4.8 (1M context) --- dynamic_offline.plan.md | 759 ++++++++++++++++++++++++++++++---------- 1 file changed, 570 insertions(+), 189 deletions(-) diff --git a/dynamic_offline.plan.md b/dynamic_offline.plan.md index e0ca48e2c..2ed3ec520 100644 --- a/dynamic_offline.plan.md +++ b/dynamic_offline.plan.md @@ -9,6 +9,7 @@ - [Portable Flag Configuration RFC](https://docs.google.com/document/d/1OWNBtXtSk535VXqf-9fqsAmU9W8kpFLAwxYi2y1qyQQ/edit?pli=1&tab=t.0#heading=h.n52036mkzewg) (local snapshot: `./Portable-Flag-Configuration-RFC.md`, repo root, untracked) — defines the building blocks: `ConfigurationWire`, `configurationFromString/ToString`, `CoreProvider`, fetch fns, hooks. - [Offline Initialization for Feature Flagging RFC](https://docs.google.com/document/d/1q1GlEbAgCGuO1OWfGbmKQkk5Oo-rE7YQwq29kMJJ4II/edit?pli=1&tab=t.0#heading=h.rnd972k0hiyer) (local snapshot: `./Offline-Initialization-for-Feature-Flagging.md`, repo root, untracked) — offline recipes built from those blocks; the operation is always `configurationFromString(wire) → provider.setConfiguration(config) → evaluate(...)`. - [ConfigurationWire (Confluence)](https://datadoghq.atlassian.net/wiki/spaces/PANA/pages/5141725646/ConfigurationWire) — the published wire spec. Its **protobuf/base64** rules encoding is the intended target (see §2.5), even though the current code still uses JSON. +- **RFC: Obfuscation for rules-based client configs** (local: `./RFC_Obfuscation_for_rules-based_client configs.md`, 2026-07-10, first draft, one approval) — defines the obfuscation design for client rules: per-flag opt-in switch, salted `ONE_OF_SHA256`/`NOT_ONE_OF_SHA256` operators (server-compatible, engine-evaluated), binary structure format, and "document what's exposed". Answers most of our obfuscation open question — see G6/D7. --- @@ -25,7 +26,8 @@ FFL-2837 adds the **rules-based** (dynamic) offline flow: - `setContext` / `onContextChange` **must not fetch**. It re-evaluates the loaded rules against the new context locally and updates the served values. - Rules-based config is **context-agnostic**: any context is valid; no "mismatch → ERROR". -- Evaluation logic is **imported from `@datadog/flagging-core`** (`evaluate()` / the rules engine), +- Evaluation logic is **imported from `@datadog/flagging-core`** (the already-published + `evaluateRulesBasedConfiguration()` rules engine — not the combined `evaluate()`; see D4/A), not reimplemented in RN. Both flows coexist behind the same `DatadogOfflineOpenFeatureProvider`. A wire may carry @@ -38,26 +40,43 @@ Per the Offline-Init RFC this is **not** a new SDK mode or a dedicated `offlineI ## 2. Current state of `@datadog/flagging-core` -### Published / consumed today -- RN depends on `@datadog/flagging-core@~2.0.1` (`packages/core/package.json:119`, `yarn.lock` → `2.0.1`). -- **2.0.1 has NO rules support.** It exposes only: - - `FlagsConfiguration = { precomputed?: PrecomputedConfiguration }` (no rules slot), - - `configurationFromString` / `configurationToString` handling **only** the precomputed branch, - - `PrecomputedConfiguration`, `PrecomputedConfigurationResponse`, `PrecomputedFlag` types, - - no `evaluation/` module. - -### In flight in PR #336 (`packages/core`) — NOT yet released -- **`configuration/configuration.ts`** adds - `RulesBasedConfiguration = { response: UniversalFlagConfigurationV1; fetchedAt?; etag? }` and - `FlagsConfiguration.rulesBased?: RulesBasedConfiguration`. -- **`configuration/wire.ts`** — `configurationFromString/ToString` round-trip a `rulesBased` branch - (`response` via `JSON.parse` / `JSON.stringify`). -- **`evaluation/`** rules engine, exported from `evaluation/index.ts`: - - `evaluate(config, type, flagKey, defaultValue, context, logger)` — shared entry point. - - `evaluateRulesBasedConfiguration(...)`, `evaluateForSubject(...)` - - `ufc-v1.ts` — `UniversalFlagConfigurationV1`, `Flag`, `Allocation`, `Split`, `Shard`, `VariantType`, `variantTypeToFlagValueType` - - `rules.ts`, `matchesShard.ts`, `sharders.ts` (targeting + bucketing) - - `errors.ts` (`TargetingKeyMissingError`), `evaluationMetadata.ts` +> ⚠️ **Corrected 2026-07-22 after code review** (verified against the installed +> `node_modules/@datadog/flagging-core@2.0.1` and PR #336, not a stale local branch). An earlier +> draft wrongly claimed 2.0.1 had "no evaluation module" — it does. See the corrected split below. + +### Published / consumed today (2.0.1) — what already ships +Verified in the installed package's `esm/` tree. **2.0.1 already contains the full rules engine and +root-exports it** (`esm/index.js`: `export * from './evaluation'` etc.): +- `evaluation/` — `evaluateForSubject`, `evaluateRulesBasedConfiguration`, `rules` (incl. `ONE_OF`, + `MATCHES` → `new RegExp(...)`), `matchesShard`, `sharders`, `ufc-v1` (`UniversalFlagConfigurationV1`, + `Flag`, `Allocation`, `Split`, `Shard`, `VariantType`, `variantTypeToFlagValueType`), `errors` + (`TargetingKeyMissingError`), `evaluationMetadata`. +- `obfuscation` — **generic MD5 helpers only** (`getMD5Hash`, `buildStorageKeySuffix`); no + obfuscated-rules decoder or context-transform pipeline (bears on G6/D7). +- `spark-md5` is a runtime dependency of 2.0.1. +- `configurationFromString` / `configurationToString` handling **only** the precomputed branch. +- `FlagsConfiguration = { precomputed?: PrecomputedConfiguration }` — **no `rulesBased` slot**. +- **Single `.` package export** — no subpaths (bears on the bundle discussion, D5). + +**Consequence for bundling:** RN's `packages/core/src/flags/configuration/wire.ts` already imports the +flagging-core **root barrel** (`configurationFromString`), and `core/src/index.tsx` re-exports it — so +under Metro (no tree-shaking) the entire engine + `spark-md5` **already ships in every +`@datadog/mobile-react-native` consumer today**, precomputed or not. (See D5 — this guts the old +"rules engine taxes every app" premise.) + +### What 2.0.1 is MISSING — added by PR #336 (unmerged, CONFLICTING/REVIEW_REQUIRED as of 2026-07-15) +Three things, all in `packages/core`: +- **`configuration/wire.ts`** adds the `rulesBased` wire branch — **the one RN actually needs.** +- **`configuration/configuration.ts`** adds `RulesBasedConfiguration` and `FlagsConfiguration.rulesBased?` + (the parsed-config slot RN reads; RN keeps the type internal — E). +- **`evaluation/evaluation.ts`** adds the combined **`evaluate(config, …)`** (precomputed-first → rules). + **RN does NOT use `evaluate()`** — D4 does path selection in RN and calls the rules-only + `evaluateRulesBasedConfiguration` (already in 2.0.1). So `evaluate()` is not an RN prerequisite (A). + +So the bump RN needs is for the **rules wire parsing + the `rulesBased` parsed slot** — not the engine +and not `evaluate()`. + +### Browser analog - **`browser/src/openfeature/core-provider.ts`** — the browser `CoreProvider`, the closest analog to the RN target. Behaviors to mirror (all confirmed by the RFC §CoreProvider): - `getConfigurationError()` errors only when there is **no** evaluatable config, or a precomputed @@ -65,14 +84,27 @@ Per the Offline-Init RFC this is **not** a new SDK mode or a dedicated `offlineI - `onContextChange` just stores the new context (no fetch); evaluation reads it live. - `setConfiguration` emits `Ready` / `ConfigurationChanged` / `Error`. -### `evaluate()` precedence (PR #336 code == RFC §"Evaluation path selection") -1. `precomputed` present **and** context matches → serve precomputed. -2. Else `rulesBased` present → `evaluateRulesBasedConfiguration(...)` (per-call, per-context). +### Path-selection precedence (RFC §"Evaluation path selection") — RN implements this itself +The combined `evaluate()` encodes this order, but **RN does not call `evaluate()`** (D4/A) — it reproduces +the order in `FlagsClient` and calls the rules-only `evaluateRulesBasedConfiguration()` for step 2: +1. `precomputed` present **and** context matches → serve precomputed (from RN's decoded `Map`). +2. Else rules present → `evaluateRulesBasedConfiguration(rulesResponse, …)` (per-call, per-context). 3. Else `precomputed` present (mismatch, no rules) → `ERROR / INVALID_CONTEXT`. 4. Else → `ERROR / PROVIDER_NOT_READY`. +The match in step 1 must be evaluated **per resolution** (R12/B), not frozen at reconcile. -Rules evaluation folds `targetingKey` into `subjectAttributes.id` and returns `ResolutionDetails` -(value, variant, `flagMetadata { allocationKey, variationType, doLog }`, reason, errorCode). +Rules evaluation folds `targetingKey` into `subjectAttributes.id` **only when `subjectKey != null`** +and returns `ResolutionDetails` (value, variant, reason, errorCode, and `flagMetadata` with +`allocationKey`, `variationType`, `doLog`, `__dd_split_serial_id`, `__dd_allocation_key`, `__dd_do_log`, +and an eval timestamp). **`flagMetadata` does NOT include `extraLogging`** (PR #336 deliberately omits it) +— see G4/D3, this blocks faithfully rebuilding a `FlagCacheEntry` for native tracking. + +**Targeting-key semantics (verified in `evaluateForSubject.js`):** `TargetingKeyMissingError` is thrown +only when `subjectKey == null` (null/undefined) **and** a matching allocation has shards. An empty +string `''` is **not** null — it is bucketed as a real (anonymous) subject. See G8/D8: RN's +`EvaluationContext.targetingKey` is typed `string` (required) and its own docs tell callers to pass +`''`, so RN currently cannot express "missing" as `undefined`; and RN's `FlagErrorCode` +(`types.ts:168`) has no `TARGETING_KEY_MISSING` member. --- @@ -92,12 +124,22 @@ The ConfigurationWire spec is the target. The current flagging-core code takes a not the destination. Treat protobuf as the plan of record; do **not** design RN around the JSON shortcut persisting. +> **Note:** the Obfuscation RFC's "binary format" (structure obfuscation) is *aligned with* but does +> **not by itself establish** protobuf. Protobuf is a separate ConfigurationWire decision, and its schema +> must be extended to carry the new **`ONE_OF_SHA256`/`NOT_ONE_OF_SHA256` operators and their salt +> fields** (G12). Don't conflate "binary" with "protobuf is decided". + Implications for RN (all deferred until flagging-core lands the protobuf path): -- **Decoding stays in flagging-core, not RN.** RN never parses the wire itself — it delegates to - flagging-core's `configurationFromString`. When flagging-core switches the rules branch from +- **Not an RN implementation blocker** — it is a **flagging-core release-contract** item. RN only + consumes the opaque parser (`configurationFromString`), so as long as the released API stays opaque, + RN neither knows nor cares whether the payload is JSON or protobuf. RN can build and test against + whatever the pinned version emits. +- **Decoding stays in flagging-core, not RN.** When flagging-core switches the rules branch from `JSON.parse` to protobuf-decode, RN inherits it via the version bump with **no RN parsing code**. -- **Watch the transitive dep.** A protobuf runtime (e.g. `protobufjs` or generated decoders) will be - pulled in transitively — folds directly into the bundle-size / Hermes review (G5, R4/R5). +- **Watch the transitive dep.** A protobuf runtime (e.g. `protobufjs` or generated decoders) would be + pulled in transitively — folds into the Hermes+JSC review (R5). It is **one of two** genuinely new bundle + masses (the other is the synchronous SHA-256 for obfuscation, G11); the evaluation engine already ships + (§2, D5). - **The `.proto` schema is a prerequisite** and does not exist in a consumable form yet — track it as part of G2 below. @@ -111,12 +153,18 @@ exported type/name before wiring RN types (R8), and never hard-code the wire fie | # | Gap | Blocking? | Action | |---|-----|-----------|--------| -| G1 | rules support is unmerged (PR #336) and unpublished | **Yes** | Land PR #336's `packages/core` changes, publish a new `@datadog/flagging-core` (>= 2.1 / 3.0). RN bumps the dep. Until then develop against a linked / `npm pack` build. | -| G2 | Rules `response` protobuf encoding not yet implemented | **Yes (upstream)** | Intended encoding is **protobuf/base64** (Confluence); current code `JSON.parse`s it. flagging-core must (a) publish the `.proto` schema and (b) switch its rules branch to protobuf-decode. RN needs **no parser** (delegates to `configurationFromString`) but inherits the transitive protobuf runtime → feeds G5/R4/R5. Until then, dev/tests run against the interim JSON shape. | -| G3 | Rules engine / UFC types exported from package root | **Yes** | Confirm `evaluate`, `UniversalFlagConfigurationV1`, `RulesBasedConfiguration` are re-exported (`core/src/index.ts` re-exports `./evaluation` and `./configuration`). Verify after publish. | -| G4 | Exposure/telemetry parity for the rules path | Partial | `evaluate()` returns only `ResolutionDetails`; it does **not** call RN native exposure tracking. RN synthesizes the fields `trackEvaluation` needs (see §4 step 5). Confirm `doLog` gating (allocation-level) with flagging-core owners. | -| G5 | Bundle size / RN + Hermes compat of the engine | **Medium** | RFC flags the rules evaluator as materially larger than precomputed lookup and proposes split entrypoints (`.../precomputed` vs full `CoreProvider`). See §4 step 6 (bundle decision) and R5. Verify `sharders.ts` hashing runs under Hermes (no Node `crypto`/browser-only APIs). | -| G6 | Obfuscated / hashed rules payloads | **No (D7)** | Obfuscation is an **upstream (flagging-core) concern**. If a customer enables it, they pre-hash their evaluation context with the same function used to hash the rules, and the engine does hashed equality / `ONE_OF` comparisons. **RN needs no special handling and no rejection path** — assume it works; just ensure RN's context normalization passes string values through untouched (test it). | +| G1 | `rulesBased` wire parsing + parsed-config slot are unmerged (PR #336) and unpublished | **Yes** | The engine **and the rules evaluator (`evaluateRulesBasedConfiguration`) already ship in 2.0.1** (§2, A). Only the wire parsing + the `rulesBased` parsed-config slot are missing — **not `evaluate()`** (RN doesn't use it). Land PR #336's `packages/core` changes, publish. Bump the dep **in `packages/core` only** (not react-native-openfeature — G-dep note). Develop against a linked / `npm pack` build meanwhile. PR #336 is CONFLICTING/REVIEW_REQUIRED, so its shape may still shift. | +| G2 | Rules `response` protobuf encoding not yet implemented | **Upstream only (not an RN blocker)** | Intended encoding is **protobuf/base64** (Confluence); current code `JSON.parse`s it. This is a flagging-core release-contract item: it must (a) publish the `.proto` and (b) switch its rules branch to protobuf-decode. RN consumes the opaque parser and inherits the switch on bump; only the transitive protobuf runtime touches RN (Hermes review, R5). Dev/tests run against whatever the pinned version emits. | +| G3 | Root exports for the symbols RN imports | **Mostly satisfied** | 2.0.1 already root-exports `UniversalFlagConfigurationV1` **and `evaluateRulesBasedConfiguration`** — the two symbols RN actually needs. RN does **not** need `evaluate` or a public `RulesBasedConfiguration` type (kept internal — E). Only remaining check: `configurationFromString` populates the rules branch after the bump (R8). | +| G4 | **Exposure/telemetry parity — one missing field** | **Yes (blocking)** | Narrowed (H): the rules `flagMetadata` **already carries** `__dd_split_serial_id` (serialId) and `__dd_eval_timestamp_ms` (timestamp) — **only `extraLogging` is missing**. Conversely RN's `FlagCacheEntry`/native bridge have **no slot** for serialId/timestamp, so exposing those upstream would not help RN transmit them anyway. **Action:** define the *exact* native `trackEvaluation` payload the offline rules path must send, then have upstream expose **`extraLogging`** on the rules result (or provide a rules-tracking API). Also confirm whether native telemetry needs the **original UFC variation type** — the engine collapses `INTEGER`/`NUMERIC` → `'number'` (`ufc-v1.js:8`), losing the distinction the precomputed path preserves. | +| G5 | Bundle size / Hermes+JSC compat | **Low today, two future adds** | The engine + `spark-md5` **and the rules evaluator already ship** via RN's existing root-barrel import (§2), so enabling rules adds only the rules wire branch — negligible now. **Two future upstream additions are genuinely new mass:** the protobuf runtime (G2) and a **synchronous SHA-256** (G11). A precomputed-only split would help **only** if flagging-core adds subpath exports (2.0.1 has none) *and* RN reworks its `wire.ts` barrel import. Verify `sharders.ts`/`spark-md5` (and later protobuf + sync SHA) run under **Hermes and JSC**. Measure before doing anything. | +| G6 | Obfuscation operators absent → **silent fallback today** | **Yes (hard prereq)** | Obfuscation = salted `ONE_OF_SHA256`/`NOT_ONE_OF_SHA256` operators (from `ONE_OF`, server-compatible, engine-evaluated) — but they're **absent from 2.0.1**, and an unknown operator makes `isValidRule` fail → `evaluateForSubject` returns **`DEFAULT` with no error** (verified). So an obfuscated rule silently serves the coded default — could wrongly enable/disable a feature. Require: (a) SHA operators shipped upstream **before** declaring dynamic client rules supported; (b) **load-time rejection of unsupported operators as `GENERAL`**, not a normal `DEFAULT`; (c) a capability/version mechanism so the service won't send SHA operators to older SDKs; (d) tests for unknown-future operators and **cached configs used after an SDK downgrade**. | +| G11 | SHA-256 must be **synchronous** and Hermes/JSC-safe | **Yes (upstream)** | The whole eval path is synchronous (`FlagsClient.get*Details`, the OF resolvers), but Web Crypto `SubtleCrypto.digest()` is **async** — unusable here. Upstream needs a **synchronous** SHA-256 (pure-JS or WASM, no Node `crypto`, no browser-only Web Crypto, no unavailable globals), Hermes **and** JSC compatible across our supported RN range. **This is new bundle mass** — invalidates the "protobuf is the only new mass" claim (D5). Add SHA-specific release-build perf + bundle measurements. | +| G12 | The portable salted-hash **protocol is unspecified**, and malformed SHA conditions need load-time rejection | **Yes (upstream contract)** | The RFC names the operators + "random salt" but defines neither the condition schema nor the hash input encoding. Before implementation the shared contract must pin: salt length/encoding; ordering (`salt‖value` vs `value‖salt` vs framed); UTF-8 + Unicode normalization; digest encoding (hex/base64, casing, padding); how numbers/booleans stringify (`ONE_OF` uses JS `value.toString()` — **not** automatically portable); empty-string / null-or-missing attribute / `NOT_ONE_OF_SHA256` behavior; and **canonical cross-SDK test vectors**. The generator, JS evaluator, and every server/mobile evaluator must produce **identical bytes** — "RN strings pass through unmodified" is necessary but far from sufficient. **Once the schema exists, load-time validation must also reject** (before READY): missing/malformed salt; salt of wrong length/encoding or unreasonably large; digests of wrong length/encoding; non-string digest-array entries; malformed `ONE_OF_SHA256`/`NOT_ONE_OF_SHA256` condition shapes; and excessive condition/value counts that could block the JS thread. | +| G7 | Untrusted rules wire is not validated, and regex safety is not solvable in RN alone | **Yes (high)** | `evaluateRulesBasedConfiguration` derefs `config.flags[flagKey]` **before** its try/catch (throws on a malformed UFC), and `rules.ts` builds `new RegExp(...)` from wire patterns. Structural validation on load (envelope, flags map, allocations/splits/variations, shard ranges, inherited keys `__proto__`/`toString`) is doable in RN. **Catastrophic-regex (ReDoS) is NOT reliably detectable by "deep validation" alone** (F) and executing a hostile pattern in a test can hang Jest/Hermes. Require one concrete mitigation — **preferably upstream/shared**: an upstream safe-regex validation guarantee (trusted config), a static safe-regex policy/library, or a bounded regex engine. Run adversarial perf tests in an **isolated, time-bounded** process. **Clone the snapshot before freezing** (do not freeze the caller's object). | +| G8 | Missing vs anonymous targeting key is not representable in RN | **Yes** | Rules bucket `''` as a real subject and only raise `TARGETING_KEY_MISSING` for `null`/`undefined`, but RN's `EvaluationContext.targetingKey` is required `string` (docs say pass `''`) and `FlagErrorCode` lacks `TARGETING_KEY_MISSING`. Decide whether missing and anonymous-empty are distinct; if so, preserve `undefined` through the rules path and add `TARGETING_KEY_MISSING` to `FlagErrorCode`. | +| G9 | Prototype-unsafe flag lookup returns `DISABLED` for absent reserved-name flags | **Yes (bug)** | `config.flags[flagKey]` on the plain UFC object resolves `toString`/`__proto__`/`constructor` through `Object.prototype`, so an **absent** flag with such a key yields `DISABLED` instead of `FLAG_NOT_FOUND`. RN own-property-guards before delegating (`Object.hasOwn`), **or** upstream switches to an own-property check / null-prototype dict. (Note the *precomputed* path already dodges this by using a `Map`.) | +| G10 | OpenFeature type/dependency boundary is unsound | **Yes** | `packages/core` declares **no** `@openfeature/*` dependency, yet flagging-core's published `.d.ts` import `@openfeature/core` while listing it only as a **devDependency** (`flagging-core/package.json:43`). Types resolve only via hoisting. RN must not add a bare `@openfeature/*` import to core — pass structurally-compatible internal types from react-native-openfeature, or add an explicit core dep **and** have flagging-core fix its published dep (§Step 5, D11, R13). | --- @@ -130,30 +178,44 @@ Architecture we inherit: The precomputed path **precomputes a `Map`** at load and serves from it. The rules path is **lazy**: it cannot precompute all flags, so it evaluates per `getX` call against -the live context via flagging-core `evaluate()`. +the live context via flagging-core's **`evaluateRulesBasedConfiguration()`** (see the design note below). + +> **Design note — call `evaluateRulesBasedConfiguration()`, not `evaluate()` (A).** D4 does the +> precomputed-vs-rules selection **in RN**, so the combined `evaluate()` (whose whole job is that +> arbitration) is unnecessary and would risk routing precomputed data through upstream arbitration. +> `evaluateRulesBasedConfiguration(ufc, type, key, default, ofContext, logger)` is **already public in +> 2.0.1** (`node_modules/@datadog/flagging-core/esm/evaluation/evaluation.d.ts:4`) and takes the UFC +> (`config.rulesBased.response`) directly — no fake `FlagsConfiguration` to construct. This also removes +> `evaluate()` from RN's prerequisites: the bump is needed **only** for the `rulesBased` wire-parsing + +> the parsed config slot, not for the evaluator. ### Step 0 — Prereqs (§3) -- [ ] PR #336 core changes merged; `@datadog/flagging-core` published with rules support. -- [ ] Bump `@datadog/flagging-core` in `packages/core/package.json` **and** - `packages/react-native-openfeature/package.json`; update `yarn.lock`. -- [ ] Re-verify the published rules field name / version and the exported symbol names (R8). +- [ ] PR #336 core changes merged; `@datadog/flagging-core` published with the **`rulesBased` wire + parsing + parsed-config slot**. (The evaluator — `evaluateRulesBasedConfiguration` — and + `UniversalFlagConfigurationV1` already ship in 2.0.1, so `evaluate()` is **not** a prerequisite.) +- [ ] Bump `@datadog/flagging-core` **in `packages/core/package.json` only** (`:119`); update `yarn.lock`. + `packages/react-native-openfeature` has **no** direct flagging-core dependency — it consumes it + transitively through its `@datadog/mobile-react-native` peer dep (`react-native-openfeature/package.json:40,49`). +- [ ] Re-verify the published rules field name / version and that `configurationFromString` populates the + rules branch (R8). `evaluateRulesBasedConfiguration` + `UniversalFlagConfigurationV1` are already exported. +- [ ] Resolve the G4 blocker with upstream (expose `extraLogging` for rules eval) **before** building the tracking path. +- [ ] **Pin the actual post-SHA flagging-core version** for the obfuscation milestone — the one that ships + the SHA operators (G6) **and** the bundled **synchronous SHA-256** (G11) **and** the specified hash + protocol (G12). This is a *separate, later* bump from the wire-parsing bump above; obfuscated offline + rules are not supportable until it lands. Confirm no `evaluate()`/protobuf assumptions leak in. ### Step 1 — Parsed-config type surface (`core/src/flags/configuration/types.ts`) -- [ ] Re-export the rules types from flagging-core alongside the precomputed ones: - ```ts - import type { - FlagsConfiguration, - PrecomputedConfiguration, - RulesBasedConfiguration, // NEW - UniversalFlagConfigurationV1, // NEW - // ...existing precomputed types - } from '@datadog/flagging-core'; - - export type ParsedRulesBasedConfiguration = RulesBasedConfiguration; // NEW - export type ParsedUniversalFlagConfiguration = UniversalFlagConfigurationV1; // NEW - // ParsedFlagsConfiguration = FlagsConfiguration already gains `rulesBased` once bumped. - ``` -- [ ] Re-export from `flags/configuration/index.ts` and package entry `core/src/index.tsx` as needed. +- [ ] Don't export a **named** rules/UFC type: use `UniversalFlagConfigurationV1` (already in 2.0.1) as the + internal rules-response type and keep any `ParsedRulesBasedConfiguration` alias internal (not re-exported). +- [ ] **But note the opacity claim is only partial (item 2 / R16 / D10).** `ParsedFlagsConfiguration` + (= upstream `FlagsConfiguration`) is **already public** (`types.ts:47` → `index.tsx:128`) and is the + type customers receive from `configurationFromString` and pass to `setConfiguration`. Once upstream adds + `FlagsConfiguration.rulesBased`, that public alias **structurally exposes** `rulesBased.response` (the + UFC) — exactly as it already exposes `precomputed.response` today — so *not* exporting the sub-type does + **not** make the schema opaque. Resolve **D10**: either (a) **accept structural visibility** and soften + the "opaque" language (pragmatic — precomputed is already visible), or (b) redesign + `ParsedFlagsConfiguration` as a **branded/opaque** type (a breaking type-compat change) so customers can + pass it through but not inspect/construct it. Do not claim opacity while shipping (a). ### Step 2 — Wire parsing (`core/src/flags/configuration/wire.ts`) - [ ] **No RN code change expected**: RN re-exports `configurationFromString`/`configurationToString` @@ -164,118 +226,247 @@ the live context via flagging-core `evaluate()`. field-name churn in R8). Use whatever encoding the pinned flagging-core version emits (JSON interim, protobuf once landed). -### Step 3 — `LoadedConfigurationState` + `loadConfiguration` (`core/src/flags/FlagsClient.ts`) +### Step 3 — ONE precise state model + `loadConfiguration` (`core/src/flags/FlagsClient.ts`) `loadConfiguration` already carries a **"FORWARD-COMPAT SEAM"** comment (FlagsClient.ts:247-255) saying -rules must be handled **before** the precomputed guard. Implement it: -- [ ] Add a variant to `LoadedConfigurationState` (no decoded `Map` — rules are lazy): - ```ts - | { kind: 'rulesBased'; configuration: ParsedRulesBasedConfiguration } - ``` -- [ ] In `loadConfiguration`, **before** the `!precomputed` guard: - ```ts - if (configuration?.rulesBased) { - // Optionally validate the UFC envelope (flags is an object, etc.). - return { kind: 'rulesBased', configuration: configuration.rulesBased }; - } - ``` -- [ ] Decide precedence when **both** present (mirror `evaluate()` / RFC): precomputed-if-context-matches - wins, else rules. Simplest faithful port: keep the whole `FlagsConfiguration` on the state (or a - `both` kind) and let `evaluate()` arbitrate (§Step 6). +rules must be handled **before** the precomputed guard. **Define one unambiguous state model** — the +earlier draft was self-contradictory (a `kind: 'rulesBased'` state vs. "let `evaluate()` arbitrate over +the full config"; these disagree on which path serves a matching precomputed config). +- [ ] Model: **retain the whole parsed `FlagsConfiguration`** plus an **optional validated precomputed + `Map`** (decoded once via `decodePrecomputedFlags`) plus a **validated rules snapshot** (`UniversalFlagConfigurationV1`, G7). + Selection order mirrors the combined `evaluate()`: matching precomputed → rules → error — but RN + implements it itself and calls only the **rules-only** `evaluateRulesBasedConfiguration()` (A). Serve + precomputed from the `Map`; enter rules only when precomputed is absent/mismatched. **Decide the match + per resolution, not once at reconcile** (B) — see Step 5. `reconcile()` still sets overall + readiness/`configurationStatus`, but must not freeze the precomputed-vs-rules choice against a context + a later hook-mutated resolution won't share. +- [ ] **Mixed-validity — split by failure stage (D).** `configurationFromString` in PR #336 parses + **both** branches inside **one** `try/catch` (`JSON.parse(precomputed.response)` **and** + `JSON.parse(rulesBased.response)`), and the `catch` returns `{}`. So: + - **Parse-time corruption** (either `response` is not valid JSON/protobuf) → the **whole wire collapses + to `{}`**; RN never receives the valid sibling. This is **atomic** unless flagging-core switches to + per-branch parsing — RN cannot isolate it. Classify the empty result as `GENERAL` (as today). + - **Structurally-invalid-but-decoded branch** (JSON parsed, but the UFC/precomputed shape is bad) → RN + **can** isolate this during its own per-branch validation (Step 3 model / G7): keep the valid branch + servable, mark the bad branch unusable, and never silently fall back to it. + Enumerate both stages in tests; do not promise recovery of a parse-corrupted sibling. +- [ ] Validate the rules snapshot on load (G7) — do not store an unvalidated UFC that a later + evaluation will throw on. Rules are handled **before** the `!precomputed` guard so they are never + misclassified as `GENERAL`. +- [ ] **Handle unsupported operators — but push validation to the right layer (G6 — critical).** The + engine treats an unknown operator (e.g. `ONE_OF_SHA256` on an SDK without it) as an invalid rule and + **silently falls back to `DEFAULT` with no error** (only literally *fail-open* if that coded default is + permissive; otherwise it's a silent wrong value). This must not reach a silent default — but **RN should + not hand-maintain a "known operators" set**: it would duplicate flagging-core's schema and drift (reject + an operator the upgraded evaluator supports; accept one whose impl is absent/incompatible; and the future + protobuf decoder may map unknown enums to a number / `UNSPECIFIED` / drop them before RN ever sees a + string). Preferred fixes, in order: + 1. **Upstream** exposes `validateRulesConfiguration()` / a capabilities API, **or** the evaluator/parser + surfaces an unsupported operator as `GENERAL` instead of a silent `DEFAULT`. + 2. If RN needs a temporary guard, **derive it from the pinned `OperatorType` enum** (exported by + flagging-core, `rules.d.ts:3`) — never a separately maintained list — and reject unknowns as `GENERAL`. +- [ ] **Capability negotiation needs a concrete owner:** + - **Official fetch path:** the request advertises the evaluator's capabilities; the service omits or + rejects flags the SDK can't evaluate. + - **Portable/offline wire (this provider):** embed required capabilities / a minimum evaluator version in + the wire, **or** guarantee the parser preserves-and-rejects unknown operators. (Owner TBD with Core.) +- [ ] **Unsupported operators must fit the branch-level validity model (reconciles the tension with the + mixed-validity rule above).** An unsupported operator invalidates the **rules branch**, not necessarily + the whole config. Define the matrix: + - **Rules-only + unsupported operator** → `GENERAL`. + - **Valid precomputed + unsupported rules:** the rules branch is unusable but the precomputed branch is + **retained and served when its context matches** (consistent with the mixed-validity decision) — do + **not** reject everything. + - **Same, context mismatched** (so precomputed can't serve and rules are unusable) → `GENERAL` (the + config is genuinely unusable here), **not** `INVALID_CONTEXT`. + - **Per-resolution (B):** provider stays `READY` while precomputed matches; a resolution whose (possibly + hook-mutated) context falls through to the invalid rules branch returns `GENERAL`. + - **Granularity:** invalidate the **whole rules branch** (simplest, safest) rather than per-flag — a + single unsupported operator means RN can't trust that branch's evaluation. (Revisit only if per-flag + isolation is later justified.) +- [ ] **Validate SHA condition shape at load once the protocol exists (G12).** Beyond "is this operator + known", a `ONE_OF_SHA256`/`NOT_ONE_OF_SHA256` condition carries a salt + digest array that is untrusted + wire data. Reject before READY: missing/malformed/oversized salt, wrong salt or digest length/encoding, + non-string digest entries, malformed condition shapes, and excessive condition/value counts (thread-block + guard). Same predictable-failure discipline as the precomputed decoder. ### Step 4 — `reconcile()` for rules (`core/src/flags/FlagsClient.ts`) -- [ ] `kind === 'rulesBased'`: **any** external context is valid (no `INVALID_CONTEXT`). Set - `evaluationContext` to the external override or an empty `{ targetingKey: '', attributes: {} }` when - none is set. `configurationStatus = 'ready'`. Do **not** populate `flagsCache` (lazy). -- [ ] Leave the precomputed branch unchanged. If storing both, the mismatch→error branch fires **only** +- [ ] When the active path is rules: **any** external context is valid (no `INVALID_CONTEXT`). + `configurationStatus = 'ready'`. Do **not** populate `flagsCache` (lazy). +- [ ] **Targeting-key decision (G8) — do NOT blindly manufacture `{ targetingKey: '' }`.** `''` buckets + as a real anonymous subject, so every keyless context would bucket **identically** and a "missing key" + could never surface `TARGETING_KEY_MISSING`. Decide the semantics: + - If missing ≡ anonymous-empty is acceptable, keep `''` and document that keyless == one shared bucket. + - If they must differ, thread `undefined` (not `''`) into the rules path when no key is set — which + requires relaxing the internal context typing and adding `TARGETING_KEY_MISSING` to `FlagErrorCode`. +- [ ] Leave the precomputed branch unchanged. The precomputed mismatch→`INVALID_CONTEXT` fires **only** when there is no rules fallback (mirror browser `getConfigurationError`). ### Step 5 — Evaluation path (`core/src/flags/FlagsClient.ts` `getDetails`) Largest change. `getDetails` currently serves from `flagsCache` (precomputed/online). -- [ ] Branch on loaded kind. For rules, call flagging-core `evaluate()`: +- [ ] For rules, call **`evaluateRulesBasedConfiguration()`** (rules-only; A) with the UFC directly: ```ts - import { evaluate } from '@datadog/flagging-core'; - const details = evaluate(flagsConfig, type, key, defaultValue, toOFContext(this.evaluationContext), logger); + import { evaluateRulesBasedConfiguration } from '@datadog/flagging-core'; + const details = evaluateRulesBasedConfiguration(rulesResponse, type, key, defaultValue, ofContext, logger); ``` - Note: `evaluate` takes an **OpenFeature-flat** context (`{ targetingKey, ...attrs }`), while - `FlagsClient` holds `{ targetingKey, attributes }`. Add an adapter (inverse of `toDdContext` / - `normalizeWireContext`); flagging-core folds `targetingKey → subjectAttributes.id` internally. -- [ ] Map flagging-core `ResolutionDetails` → RN `FlagDetails` (value, variant, `allocationKey` from - `flagMetadata`, reason, errorCode/errorMessage). Let `evaluate` produce `TYPE_MISMATCH`/`FLAG_NOT_FOUND`. -- [ ] **Exposure tracking (DECIDED — D3):** rules `evaluate()` has no tracking side effect. Synthesize a - `FlagCacheEntry` from the resolution (`key`, `value`, `variationKey`=variant, `allocationKey`, - `variationType` from `flagMetadata`, `variationValue`=stringified value, `reason`, `doLog`, - `extraLogging`) and call `this.track(entry, this.evaluationContext)` **only when `doLog` is true**. - Separately, confirm whether the precomputed path's *unconditional* `track` is correct (native fetch - may already pre-filter to loggable flags) or a pre-existing bug to align — track as a follow-up, do - **not** silently change precomputed behavior here. + `rulesResponse` is `configuration.rulesBased.response` (a `UniversalFlagConfigurationV1`). It takes an + **OpenFeature-flat** context (`{ targetingKey, ...attrs }`) while `FlagsClient` holds + `{ targetingKey, attributes }` — add an adapter (inverse of `toDdContext`/`normalizeWireContext`); the + engine folds `targetingKey → subjectAttributes.id` only when non-null (G8). +- [ ] **Select the path against the RESOLUTION context, not the stored one (B).** `resolveX` currently + discards its `context` arg (`coreProvider.ts:61`) and `FlagsClient` uses the stored context. But a + **`before` hook can mutate the evaluation context for a single resolution** (see OpenFeature context + spec), so the context actually being evaluated can differ from the one `reconcile()` last saw. If the + precomputed context-match check was decided at reconcile-time against the stored context, a hook-mutated + resolution could be served a **precomputed value for a context it does not match** — an assignment leak. + So the **precomputed-vs-rules selection (esp. the precomputed context match) must run per-resolution + against the effective resolution context**, or the provider must explicitly ignore/forbid `before`-hook + context changes. Per-resolution selection still keeps the O(1) precomputed `Map` lookup. +- [ ] **Thread the resolution context + logger — but resolve the dependency boundary first (item 5 + N).** + The web SDK is **static-context**: clients/invocations must **not** supply context (OpenFeature forbids + it), so the fix is *not* "don't drop invocation context" — there is none. Context comes from + global/domain state **plus `before`-hook mutations**, and the resolver receives that effective context. + Thread the resolver-provided context + logger into the rules evaluation (as the browser `CoreProvider` + does) and test against the client-side model (global/domain + hooks). **Boundary problem (G10/R13):** + `packages/core` has **no** `@openfeature/*` dependency (`package.json` — zero refs), and flagging-core + publishes `.d.ts` that `import @openfeature/core` while listing it only as a **devDependency** + (`flagging-core/package.json:43`) — so the `Logger`/`ResolutionDetails`/`EvaluationContext` types work + today only by **hoisting accident**. Do **not** add a bare `@openfeature/*` import to core. Pick one: + (a) keep OpenFeature types in `react-native-openfeature` and pass **structurally-compatible internal + context/logger types** into `FlagsClient` (no core dep); or (b) add an explicit `@openfeature/*` + dependency to `packages/core` **and** require flagging-core to declare `@openfeature/core` as a real + dependency. Also decide whether threading widens the **public** `FlagsClient.get*Details` API or goes + through a **separate internal entry point** — prefer the latter to avoid a public API change. +- [ ] **`id` vs `targetingKey` precedence — decide, don't just "watch" (D9/item 4).** The engine builds + `subjectAttributes = { id: subjectKey, ...remainingContext }` (`evaluation.js:15`), so a customer + **`id` attribute overrides** the synthetic targeting-key `id` used for **rule matching**, while + **sharding still uses `subjectKey`** (`selectSplitUsingSharding`) — targeting and bucketing would then + key off different identifiers. Choose and enforce a contract: **reserve `id` for the targeting key** + (drop/reject a customer `id` in the flat adapter — recommended), or deliberately document the upstream + override. Assert the chosen result in tests; do not leave it to "watch". +- [ ] Map `ResolutionDetails` → RN `FlagDetails` (value, variant, `allocationKey`/`doLog` from + `flagMetadata`, reason, errorCode/errorMessage). **`evaluateRulesBasedConfiguration` already returns + `FLAG_NOT_FOUND`** for an absent flag (`evaluation.js:21`), plus `TYPE_MISMATCH`/`DISABLED`/`DEFAULT`/ + `TARGETING_KEY_MISSING`/`GENERAL` — **map them through; RN does not synthesize `FLAG_NOT_FOUND`** + (round-2 said otherwise — corrected). +- [ ] **Prototype-safe flag lookup (G9 — bug).** The evaluator does `config.flags[flagKey]` on a plain + object, so a **missing** flag named `toString`/`__proto__`/`constructor` resolves through + `Object.prototype` (truthy) and returns **`DISABLED`, not `FLAG_NOT_FOUND`**. Before delegating, guard + with `Object.prototype.hasOwnProperty.call(rulesResponse.flags, key)` (or `Object.hasOwn`) and return + `FLAG_NOT_FOUND` yourself when it is not an own property — **or** require an upstream own-property/ + null-prototype-dict fix. Test **absent reserved-name flags** (not just malformed configs that contain + those names). +- [ ] **Exposure/telemetry tracking (CORRECTED — D3):** the native side (Android `trackResolution`, iOS + `trackEvaluation`) expects **every successful assignment** to cross the bridge and applies `doLog` + itself — `doLog` gates **only the exposure event**, while RUM (gated by `rumIntegrationEnabled`) and + evaluation telemetry (gated by `trackEvaluations`) fire independently. So call + `this.track(entry, ctx)` for **every successful assignment, NOT gated on `doLog`** — matching the + existing precomputed path (`FlagsClient.ts:441`, which is therefore correct, not a bug). Track **only + when a variant was actually assigned** — do **not** track for `DISABLED`, unmatched/no-variant + `DEFAULT`, `TYPE_MISMATCH`, `FLAG_NOT_FOUND`, or error results (matches precomputed's early returns). + **Blocked by G4:** the synthesized `FlagCacheEntry` needs `extraLogging`, which the rules + `flagMetadata` does not carry. Resolve G4 before implementing this bullet. - [ ] Keep precomputed + online paths serving from `flagsCache` untouched. -### Step 6 — Precedence when both present + bundle-size decision -- [ ] **Two paths (DECIDED — D4):** keep precomputed on the decoded `Map` + `track`; only the rules - branch calls `evaluate()`. Do **not** route precomputed through `evaluate()`. Factor the shared - `ResolutionDetails → FlagDetails` + synthesize-`FlagCacheEntry`-for-`track` mapping into one helper so - both paths reuse it. Rationale: no risk to the shipped FFL-2666 precomputed flow, keeps the O(1) - precomputed lookup, and keeps the rules engine **out** of the precomputed-only path (serves D5). - Implement the both-present order (precomputed-if-context-matches → rules) by passing the full - `FlagsConfiguration` into `evaluate()` from the rules branch and letting it arbitrate. -- [ ] **Bundle size (DECIDED — D5): accept the static import cost for MVP, measure the delta, and if it - is unacceptable do a provider split + subpath export — NOT dynamic `import()`.** Key constraint: - Metro does not do real code-splitting for standard RN app builds, so a dynamic `import()` still ships - the engine in the bundle (it only defers module *init* via `inlineRequires`) — it does **not** reduce - size on RN. So: - 1. MVP: `import { evaluate } from '@datadog/flagging-core'` statically; measure the added bytes - (engine + eventual protobuf runtime). - 2. Only if the delta is unacceptable: ship a precomputed-only provider - (`DatadogPrecomputedOfflineProvider`) that imports a flagging-core `@datadog/flagging-core/precomputed` - subpath, alongside the full rules-capable offline provider — mirrors the RFC's - `PrecomputedCoreProvider` vs `CoreProvider`. This is the *only* option that actually removes the - engine from precomputed-only bundles on RN. (This split is a **bundle-size lever only** — it is not - a security gate; see D6.) +### Step 6 — Path selection when both present + bundle-size decision +- [ ] **Two paths (DECIDED — D4), one precise per-resolution selection (see Step 3 + Step 5/B).** Serve + matching precomputed from the decoded `Map` (O(1)); take the rules branch + (`evaluateRulesBasedConfiguration(rulesResponse, …)`) **only** when precomputed is absent or its context + does not match **the effective resolution context**. Because RN calls the **rules-only** evaluator with + just the UFC, precomputed data is excluded from the upstream call **by construction** — there is no + full-config `evaluate()` arbitration to accidentally route precomputed through, and RN's + `decodePrecomputedFlags` validation is never bypassed. Do the precomputed context-match per resolution + (B), not once at reconcile. Factor the shared `ResolutionDetails → FlagDetails` + + synthesize-`FlagCacheEntry` mapping into one helper. +- [ ] **Bundle size (CORRECTED — D5): the engine already ships; measure, then likely do nothing.** The + earlier premise was wrong: RN's `wire.ts` already imports the flagging-core **root barrel**, so under + Metro the whole engine + `spark-md5` is **already in every `@datadog/mobile-react-native` bundle** + today (§2, G5). The rules evaluator (`evaluateRulesBasedConfiguration`) also already ships, so enabling + rules adds only the rules wire branch — negligible. Actions: + 1. **Measure separate baselines** (item 6): current baseline, root-SDK import, online flags, + precomputed offline, dynamic offline. Attribute the delta correctly. + 2. Dynamic `import()` is **rejected** — Metro does not code-split release bundles, so it wouldn't + shrink anything (Metro module API confirms). + 3. A precomputed-only split (`DatadogPrecomputedOfflineProvider` + a flagging-core + `@datadog/flagging-core/precomputed` subpath) is the *only* real size lever — but it helps **only + if** flagging-core adds subpath exports (2.0.1 has just `.`) **and** RN moves its `wire.ts` import + off the root barrel onto that subpath. Pursue only if the measured delta actually justifies it. + The genuinely new mass to watch is **two future upstream additions**: the protobuf runtime (G2) **and + a synchronous SHA-256 implementation** for obfuscation operators (G11) — *not* the existing engine. (This + split is a bundle lever only — not a security gate; see D6.) ### Step 7 — `DatadogOfflineOpenFeatureProvider` (`react-native-openfeature/src/offlineProvider.ts`) - [ ] `initialize` / `onContextChange` already do not fetch. For rules, `applyContext`'s reconcile returns `ready` for any context, so the mismatch-throw path disappears naturally. Verify the - empty-context re-adopt logic still reads sensibly for rules (an empty context for rules is just an - anonymous subject; there is no embedded context to re-adopt). + empty-context handling for rules — **but this depends on the unresolved D8** (missing vs anonymous): + there is no embedded context to re-adopt, so an empty context is *either* an anonymous subject (`''`) + *or* a "no key" signal, per whatever D8 decides. Do not hard-code the anonymous interpretation here + until D8 is settled. - [ ] **Update the class doc comment** — it currently states precomputed-only semantics ("you should **not** call `OpenFeature.setContext`"). For rules, `setContext` **is** the intended dynamic path. - [ ] `setConfiguration` event mapping (`Ready` / `ConfigurationChanged` / `Error`) is already generic and matches the RFC event model; confirm a rules `ready` triggers the right transitions. -- [ ] **No opt-in gate for offline rules (DECIDED — D6).** Online/offline is orthogonal to - precomputed/rules. In the offline flow the customer already holds the rules wire and explicitly loads - it via `setConfiguration`, so there is no hidden behavior to gate — loading a rules wire into the - OfflineProvider just works. The real security control lives at **fetch time** (a public client token - must not be scoped to pull private rules); that is enforced server-side and belongs to the - **OnlineProvider** rules-fetch work — **out of scope for FFL-2837**. Only obligation here: a docs - caveat that rules ship on-device and are reverse-engineerable (the customer's informed choice; base64 - is not a security control). The precomputed-only provider from D5, if built, is a bundle lever only. +- [ ] **Opt-in posture (D6 — justification splits by config source).** For **Datadog-generated** configs + the platform per-flag switch is the opt-in the Offline-Init RFC asks for + (`Offline-Initialization-for-Feature-Flagging.md:95`, `:158`), enforced server-side — but the RFC is a + first draft (per-flag vs per-org unsettled) and **scoped client tokens don't exist yet** (RFC:109). For + **customer-supplied/bundled** wires (which this provider accepts) platform controls are **bypassed**, so + `setConfiguration(rules)` is the opt-in and the customer owns supplying client-appropriate rules. Either + way the **RN offline provider needs no additional gate**. Keep product/security sign-off on record; ship + the docs caveat + "what stays visible" list regardless. ### Step 8 — Exports, examples & docs -- [ ] Export new public types (`ParsedRulesBasedConfiguration`) from - `react-native-openfeature/src/index.ts` and `core/src/index.tsx` if customers need them. -- [ ] Update package READMEs + example apps (`example/src/flags/flagsProvider.ts`, - `example-new-architecture/flags/flagsProvider.ts`) with a rules-based offline snippet +- [ ] **Do NOT export a named rules/UFC type — but that alone does not make the config opaque (E + D10).** + Keep `ParsedRulesBasedConfiguration`/`UniversalFlagConfigurationV1` unexported. **However**, the already-public + `ParsedFlagsConfiguration` structurally carries `rulesBased` after the bump (item 2 / R16), so decide + **D10** here: soften the opacity claim (accept it, as with precomputed today) or brand + `ParsedFlagsConfiguration`. Don't ship docs/marketing that call the config opaque under option (a). +- [ ] **Full two-flow README rewrite (not just a new snippet).** `react-native-openfeature/README.md` + (≈`:146`–`:169`) currently documents **precomputed-only** semantics: single-subject snapshot, the + "do **not** call `OpenFeature.setContext` with a different context" warning, the empty-context/`''` + re-adopt caveat, and the dedicated-domain/`clientName` guidance. The rules flow **inverts** much of + this (`setContext` *is* the dynamic path; there is no embedded context to re-adopt). Rewrite the + offline section to present both flows side-by-side and clearly scope each caveat to precomputed. +- [ ] Update example apps (`example/src/flags/flagsProvider.ts`, + `example-new-architecture/flags/flagsProvider.ts`) with a rules-based offline flow (`configurationFromString(wire) → setConfiguration → setContext(a) / setContext(b)`). +- [ ] Update the class doc comment on `DatadogOfflineOpenFeatureProvider` for the two-flow behavior. +- [ ] **Document precisely what stays visible in a rules config (D7 threat model) — cover ALL shipped UFC + data, not just names.** Don't call it "confidential". Per `ufc-v1.d.ts`, the config ships: flag/variant/ + attribute **names**; variant **values**; regex/numeric/version **operands**; the decodable config + **structure**; **guessable hashed membership values** (salted `ONE_OF_SHA256` resists precomputation but + not offline enumeration of low-entropy values); **and** allocation **keys**, split **serialIds**, + **`extraLogging`**, **`doLog`**, allocation `startAt`/`endAt`, shard **salts**, the **environment name**, + and **`createdAt`**. Flag `extraLogging` especially — the plan is explicitly blocking on exposing it for + tracking (G4). Consider the RFC's UI lock-icon idea. --- ## 5. All imports added to dd-sdk-reactnative -From `@datadog/flagging-core` (post-bump): -- `evaluate` — **static** value import in `core/src/flags/FlagsClient.ts` (D5: no dynamic import — it - does not reduce bundle size on Metro). -- `type RulesBasedConfiguration`, `type UniversalFlagConfigurationV1`, `type FlagsConfiguration`, - `type FlagTypeToValue` — in `configuration/types.ts` and `FlagsClient.ts`. +From `@datadog/flagging-core`: +- `evaluateRulesBasedConfiguration` — **static** value import in `core/src/flags/FlagsClient.ts` + (already public in 2.0.1; A). No dynamic import (D5: it doesn't reduce Metro bundle size). **Not + `evaluate`.** +- `type UniversalFlagConfigurationV1` (already exported by 2.0.1) — the internal rules-response type in + `configuration/types.ts` / `FlagsClient.ts`. `type FlagsConfiguration` / `FlagTypeToValue` as today. - (already imported) `configurationFromString`, `configurationToString`, precomputed types. -- Possibly `type ResolutionDetails`, `type Logger` if threaded (or keep using `@openfeature/web-sdk`). +- **`Logger`/`ResolutionDetails` — NOT imported bare into core (D11).** `packages/core` has no + `@openfeature/*` dep and flagging-core mis-declares `@openfeature/core` as a devDep (G10). Per D11: + either pass **structurally-compatible internal** context/logger types from react-native-openfeature into + an **internal** `FlagsClient` entry point, or add an explicit core `@openfeature/*` dep + fix flagging-core's + published dep. Do not widen `FlagsClient.get*Details` for this. +- **Kept internal, not re-exported:** any `RulesBasedConfiguration`/`ParsedRulesBasedConfiguration` alias (E). Internal (RN) new: - A context adapter (RN `{targetingKey, attributes}` ↔ OpenFeature-flat `{targetingKey, ...attrs}`), likely in `flags/configuration/context.ts` or `flags/internal.ts`. -- `ParsedRulesBasedConfiguration` type export chain (types.ts → configuration/index.ts → index.tsx). +- An internal `ParsedRulesBasedConfiguration` alias used only within `flags/` (no public export chain — E). **No new native surface required** — `NativeDdFlags.trackEvaluation(clientName, key, rawFlag, targetingKey, attributes)` already accepts a synthesized flag object, and rules evaluation is entirely JS. +**Caveat (G4):** `rawFlag` must carry `extraLogging`, which the rules result does not expose today — +resolve upstream first, and confirm whether the collapsed `INTEGER`/`NUMERIC` → `number` variation type +is acceptable for native telemetry. --- @@ -305,18 +496,63 @@ Model on existing suites: `react-native-openfeature/src/__tests__/offlineProvide matches an allocation; return the else/fallthrough variant otherwise. - [ ] **Dynamic behavior:** changing context between two evaluations yields **different** values when the two subjects bucket differently (the core point of FFL-2837). -- [ ] Flag-not-found → `FLAG_NOT_FOUND` + default. Type mismatch → `TYPE_MISMATCH` + default. -- [ ] Missing targeting key where a rule requires it → `TARGETING_KEY_MISSING`/`INVALID_CONTEXT` + default. -- [ ] Exposure (D3): `native.trackEvaluation` called with a correctly synthesized flag (`variationKey`, - `allocationKey`, `variationValue` string) **when `doLog` is true; not called when `doLog` is false**. -- [ ] Hashed/obfuscated (D7): a context with string attribute values (as a customer would supply - pre-hashed) survives `processEvaluationContext`/the OpenFeature-flat adapter **unmodified**, so - hashed equality / `ONE_OF` matching in the engine can work. +- [ ] Flag-not-found → `FLAG_NOT_FOUND` + default (the evaluator returns this directly — map it through). + Type mismatch → `TYPE_MISMATCH` + default. +- [ ] **Prototype-named absent flag (item 1 bug):** evaluating a flag key `toString` / `__proto__` / + `constructor` that is **absent** from `flags` returns **`FLAG_NOT_FOUND`** (proves the own-property + guard works) — without the guard the evaluator returns `DISABLED`. Distinct from a malformed config + that merely *contains* such a key. +- [ ] **`id`/`targetingKey` precedence (D9):** a customer `id` attribute is handled per the chosen + contract (recommended: dropped so the targeting key is the sole subject id) — assert rule matching and + sharding key off the **same** identifier. +- [ ] **Disabled flag** (`enabled: false`) → default with `DISABLED` reason. +- [ ] **No matching allocation** / after `endAt` / before `startAt` → default allocation (`DEFAULT`). +- [ ] **Missing variant** (split references a variationKey absent from `variations`) → default. +- [ ] **Targeting-key semantics (G8):** `undefined`/null key with a sharded matching allocation → + `TARGETING_KEY_MISSING` **only if** RN decides missing≠anonymous and threads `undefined`; an **empty + string `''` buckets as a real subject** (assert two distinct empty-key contexts bucket the same). Do + not assert `TARGETING_KEY_MISSING` for `''` — it cannot occur. +- [ ] **Per-resolution path selection (B):** with **both** precomputed (matching stored context) and + rules loaded, a `before` hook that mutates the resolution context so it **no longer matches** the + precomputed snapshot must **not** be served the precomputed value — it must fall through to rules (or + error), i.e. the precomputed match is re-checked against the resolution context, not the stored one. +- [ ] Exposure/telemetry (D3): `native.trackEvaluation` called for **every successful assignment + regardless of `doLog`** (verify `doLog:false` still crosses the bridge so native RUM/eval telemetry + fire); assert the synthesized flag carries `variationKey`, `allocationKey`, `variationValue` string, + and `extraLogging` (blocked on G4). +- [ ] **Negative tracking (J):** `native.trackEvaluation` is **NOT** called for `DISABLED`, + unmatched/no-variant `DEFAULT`, `FLAG_NOT_FOUND`, `TYPE_MISMATCH`, or error results — only a real + assigned variant tracks (matches precomputed's early returns). +- [ ] **Validation / hostile input (G7):** malformed envelope (no `flags`), malformed flag, bad + shard/range, inherited-key flag (`toString`/`__proto__`), and **post-load mutation** of the passed + config object — each fails predictably (default + error, not a throw/hang from a READY provider). +- [ ] **Catastrophic regex (G7/F) — run in an isolated, time-bounded process** (a hostile pattern can + hang Jest/Hermes). Assert the chosen mitigation (upstream safe-regex guarantee / static policy / + bounded engine) holds; do not rely on structural validation to detect ReDoS. +- [ ] **Mixed configs (D):** (a) **parse-time** corruption of either branch collapses the whole wire to + `{}` (atomic — assert the valid sibling is *not* recoverable today); (b) **structurally-invalid but + decoded** branch is isolated — the valid sibling stays servable, the bad branch is never a silent fallback. +- [ ] Context threading uses the client-side model (global/domain + `before` hooks; no client/invocation + context — C), and the OpenFeature types reach `FlagsClient` via the chosen dependency boundary (D11), + not a bare `@openfeature/*` import in core. +- [ ] **Unsupported operator fails closed (G6):** a rule with an operator RN doesn't recognize + (`ONE_OF_SHA256` today, or any future operator) is **rejected at load as `GENERAL`** — assert it does + **not** silently evaluate to `DEFAULT`. Include a **cached-config-after-downgrade** case (a config with + newer operators loaded by an older SDK build). +- [ ] Obfuscation (D7): (a) attribute string values survive `processEvaluationContext` / the flat adapter + **unmodified** so the engine can hash them; (b) once upstream ships `ONE_OF_SHA256`/`NOT_ONE_OF_SHA256` + with a specified protocol (G12), evaluate the **canonical cross-SDK test vectors** (salt, stringification + of numbers/booleans, empty/null, `NOT_ONE_OF_SHA256`) — byte-identical to the generator/server, not just + "strings pass through". Until then those operators are an upstream gap (absent from 2.0.1). +- [ ] **Evaluation hot-path performance:** rules `getX` (incl. the sync SHA-256 once present) is fast + enough for repeated per-render calls; measure on a release build under Hermes and JSC. ### Provider (react-native-openfeature) - [ ] Rules config loaded before registration → provider reaches `READY` (no context needed). -- [ ] `setContext(ctxA)` then `setContext(ctxB)` re-evaluates locally, **no fetch**, emits expected - lifecycle (RECONCILING→READY), values reflect each context. +- [ ] `setContext(ctxA)` then `setContext(ctxB)` re-evaluates locally, **no fetch**, values reflect each + context. Do **not** assert a `RECONCILING→READY` sequence: `onContextChange` is **synchronous** + (`offlineProvider.ts:103`) and intentionally skips the transient `RECONCILING` state; assert the + final `READY`/updated values and that no fetch occurred instead. - [ ] `setConfiguration` valid rules → `CONFIGURATION_CHANGED` (and `READY` if recovering from error). - [ ] `setConfiguration` empty/invalid → `PROVIDER_ERROR` with a top-level errorCode. - [ ] Determinism: same (context, config) → same bucketed variant across calls. @@ -326,66 +562,211 @@ Model on existing suites: `react-native-openfeature/src/__tests__/offlineProvide - [ ] End-to-end: parse a real rules `ConfigurationWire` sample → set provider → evaluate several flags across several contexts; assert values + exposure calls. Use a UFC fixture from ffe-service or the flagging-core test fixtures. -- [ ] **Bundle-size check** (R5): measure the delta the rules engine adds; confirm the precomputed-only - path does not regress (validates the §Step 6 decision). -- [ ] **Hermes smoke test**: rules evaluation (incl. `sharders` hashing) runs under Hermes. +- [ ] **Bundle-size check** (D5/G5): measure **separate baselines** — current baseline, root-SDK import, + online flags, precomputed offline, dynamic offline — and attribute the delta correctly (the engine + already ships, so the *incremental* rules cost is near-zero; the **real future adds are the protobuf + runtime (G2) and the synchronous SHA-256 (G11)** — measure each against the pinned post-SHA version). +- [ ] **Hermes AND JSC smoke test**: rules evaluation (incl. `sharders`/`spark-md5`, the future protobuf + runtime, and the future synchronous SHA-256) runs under **both engines** across the supported RN range; + confirm no Node `crypto` / browser-only Web Crypto / unavailable globals. +- [ ] **Integration prerequisites checklist** must include: the synchronous SHA dependency (G11), Hermes + **and** JSC coverage, and the **actual post-SHA flagging-core version** (Step 0) — not "protobuf only". --- ## 7. Risks & unknowns -1. **flagging-core rules support is unmerged/unpublished (G1, G3).** Everything blocks on PR #336 - landing + a release. Mitigate: develop against a linked / `npm pack` build; keep the RN diff +1. **Rules wire parsing + parsed-config slot are unpublished (G1, G3).** The engine **and the rules + evaluator (`evaluateRulesBasedConfiguration`) already ship** in 2.0.1; only the wire parsing + slot + (PR #336) block — **not `evaluate()`** (RN doesn't use it). PR #336 is **CONFLICTING / REVIEW_REQUIRED**, + so its shape may still move. Mitigate: develop against a linked / `npm pack` build; keep the RN diff isolated so the dependency bump is the only integration point. 2. **Two evaluation paths in `FlagsClient` (DECIDED D4 — keep two).** Precomputed serves from a decoded - `Map`; rules evaluate lazily via `evaluate()`. Residual risk: divergent reason codes / type checks - between the two — mitigated by the shared `ResolutionDetails → FlagDetails` mapping helper and the - regression tests. -3. **Exposure parity (G4).** `evaluate()` has no tracking side effect; RN synthesizes the flag object - for `trackEvaluation`. `doLog` gating (allocation-level) and RUM correlation must match the online - path. Confirm with flagging-core / ffe owners. -4. **Bundle size / tree-shaking (DECIDED D5 — accept + measure).** The rules engine (and eventual - protobuf runtime) ships in every app that imports the offline provider; Metro won't code-split it - away. Residual risk: the measured delta is unacceptable for precomputed-only users — fallback is the - provider-split + subpath export (not dynamic import). See §Step 6. -5. **Hermes/RN bundling of the engine.** `sharders.ts` hashing may assume Node/browser APIs. Verify it - runs under Hermes; add the smoke test above. -6. **Context shape adapters.** flagging-core wants OpenFeature-flat context and folds `targetingKey→id`; - RN holds `{targetingKey, attributes}`. Normalization mismatches could mis-bucket. Cover with the - determinism tests. -7. **Security posture (DECIDED D6 — no offline opt-in gate).** Offline rules are customer-supplied, so - there is nothing to gate; the real control is fetch-time API-token scoping in the OnlineProvider - (out of scope for FFL-2837). Residual: a docs caveat that rules are reverse-engineerable on-device. + `Map`; rules evaluate lazily via `evaluateRulesBasedConfiguration()`. Residual risk: divergent reason + codes / type checks — mitigated by the shared mapping helper, the explicit per-resolution + path-selection (Step 3/5), and tests. (The evaluator returns `FLAG_NOT_FOUND` itself; RN only adds the + own-property guard — R14.) +3. **Exposure/telemetry parity — BLOCKING (G4, CORRECTED D3, NARROWED).** Native tracks **every** successful + assignment (`doLog` gates only the exposure event; RUM + eval telemetry are separate), so RN must + `track` unconditionally on assignment. The rules `flagMetadata` **already carries** serialId + (`__dd_split_serial_id`) and timestamp (`__dd_eval_timestamp_ms`); **only `extraLogging` is missing** + — and RN's `FlagCacheEntry`/bridge have no serial/timestamp slot anyway. Define the exact native + payload, have upstream expose **`extraLogging`**, and confirm whether the `INTEGER`/`NUMERIC`→`number` + collapse matters for native telemetry. +4. **Bundle size (CORRECTED D5 — mostly a non-issue, two future adds).** The engine + `spark-md5` + the + rules evaluator **already ship** via RN's root-barrel import today, so the incremental rules cost is the + wire branch only. Two **future** upstream additions are genuinely new mass: the protobuf runtime (G2) + and a **synchronous SHA-256** for obfuscation (G11). Metro won't code-split, so dynamic import is out; a + provider/subpath split is the only real lever and only if measurement justifies it. Measure first. +5. **Hermes/JSC bundling.** `sharders.ts`/`spark-md5`, the future protobuf runtime (G2), and the future + **synchronous SHA-256** (G11) must run under **Hermes and JSC** across the supported RN range. Add smoke + tests + SHA-specific release-build perf/bundle measurements; confirm no Node `crypto` / browser-only Web + Crypto / unavailable globals. +6. **Context/logger threading + paradigm (item 5, CORRECTED C).** `resolveX` discards the OF context+logger + and evaluates against the stored context. The web SDK is **static-context** — there is no + client/invocation context; context is global/domain + `before`-hook mutations. Thread the resolver's + effective context+logger into rules eval **through a resolved dependency boundary (R13)**, settle the + `id`/`targetingKey` precedence (R15), and — see R12 — do the precomputed match against that + per-resolution context. +7. **Security / opt-in — justification splits by config source (D6).** For **Datadog-generated** configs + the platform per-flag switch is the opt-in (but the RFC is a first draft and scoped client tokens don't + exist yet, RFC:109). For **customer-supplied/bundled** wires — which this provider accepts — platform + controls are **bypassed**, so `setConfiguration(rules)` is the opt-in and the customer owns supplying + client-appropriate rules. "No SDK gate" still holds; keep sign-off on record + ship the docs caveat. 8. **Wire naming/version churn (§2.5).** `rulesBased` (code) vs `server` (RFC) vs `rules` (Confluence); - `version` 1 vs 2. The docs are drafts. Pin to the released flagging-core version, never hard-code the - field name in RN, and add a guard test. -9. **Obfuscated / hashed UFC (DECIDED D7 — upstream, assume it works).** Hashing is a flagging-core - engine concern; the customer hashes their context with the same function. RN adds no handling and no - rejection — only a test that context normalization preserves (hashed) string values. + `version` 1 vs 2. The docs are drafts. Pin to the released version, never hard-code the field name, add a guard test. +9. **Obfuscation — design known, three hard prereqs, and today it fails **open** (G6/G11/G12, D7).** + Salted `ONE_OF_SHA256`/`NOT_ONE_OF_SHA256` operators, engine-evaluated. RN needs no detection/pre-hash, + **but**: (a) the operators are absent from 2.0.1 and an unknown operator **silently returns `DEFAULT`** + (not an error) — RN must reject unknown operators at load as `GENERAL` and upstream must add them + + capability/version negotiation; (b) SHA-256 must be **synchronous** and Hermes/JSC-safe — new bundle + mass (G11); (c) the salted-hash **protocol is unspecified** — needs cross-SDK test vectors (G12). + **Threat model:** salt defeats precomputation/rainbow tables but does **not** hide low-entropy guessable + values from offline enumeration (NIST SP 800-132) — do not call it "confidentiality". +10. **Untrusted-wire validation + regex — high (G7, CORRECTED F).** `evaluateRulesBasedConfiguration` + derefs `config.flags[flagKey]` before its try/catch and builds `new RegExp(...)` from wire patterns, + so a malformed/hostile rules wire can throw or hang from a READY provider. Structural validation on + load is doable in RN, but **ReDoS is not reliably detectable by validation alone** — require an + upstream safe-regex guarantee / static policy / bounded engine, run adversarial tests in an isolated + time-bounded process, and **clone before freezing** the snapshot (don't freeze the caller's object). +11. **Missing vs anonymous targeting key (G8).** `''` buckets as a real subject; only null/undefined + raises `TARGETING_KEY_MISSING`. RN's required-`string` type and missing `FlagErrorCode` member make + "missing" unrepresentable. Decide the semantics (D8) before coding the context adapter. +12. **Per-resolution path selection (B).** A `before` hook can mutate the resolution context, so a + precomputed-vs-rules choice frozen at reconcile time (against the stored context) could serve a + precomputed value to a non-matching context (assignment leak). Re-check the precomputed match against + the effective resolution context on every evaluation, or explicitly ignore/forbid hook context changes. +13. **OpenFeature type/dependency boundary (N, NEW).** `packages/core` has **no** `@openfeature/*` dep, and + flagging-core ships `.d.ts` that import `@openfeature/core` while declaring it only as a devDependency — + so the shared types resolve only by hoisting accident. Threading `Logger`/`ResolutionDetails` into core + would deepen this. Fix by either passing structurally-compatible internal types from + react-native-openfeature into core (no core dep), or adding an explicit core dep **and** getting + flagging-core to publish `@openfeature/core` as a real dependency. Also decide public-API vs internal + entry point for the threaded params. +14. **Prototype-unsafe flag lookup (item 1, NEW — bug).** `config.flags[flagKey]` on a plain object means an + absent flag named `toString`/`__proto__`/`constructor` returns `DISABLED` instead of `FLAG_NOT_FOUND`. + RN must own-property-guard before delegating, or get an upstream own-property/null-prototype fix. +15. **`id` vs `targetingKey` precedence (D9, NEW).** The evaluator lets a customer `id` attribute override + the targeting-key id for **rule matching** while **sharding** uses the targeting key — split identity. + Enforce a contract (recommended: reserve `id` for the targeting key, drop a customer `id`) and test it. +16. **Public config opacity is already partial (item 2, NEW).** `ParsedFlagsConfiguration` (= upstream + `FlagsConfiguration`) is exported from the package root, so its structure — `precomputed` today, + `rulesBased` after the bump — is inspectable/constructable by TS consumers regardless of whether the + sub-types are exported. Either soften the opacity claim (accept structural visibility, as precomputed + already is) or make `ParsedFlagsConfiguration` a branded/opaque type (breaking type-compat change). --- ## 8. Decisions & remaining open questions -### Decisions (2026-07-22) -- **D3 — Exposure/`doLog`:** rules-path exposure is **gated on `doLog`** (track only when true). Precomputed's - current unconditional `track` is left as-is pending confirmation it isn't a pre-existing bug (follow-up). (§Step 5, R3) -- **D4 — Evaluation paths:** **keep two paths** — precomputed on the decoded `Map` + `track`, rules via - `evaluate()` — with a shared `ResolutionDetails → FlagDetails` mapping helper. No unification. (§Step 6, R2) -- **D5 — Bundle size:** **accept the static-import cost for MVP and measure it.** Dynamic `import()` is - rejected (Metro doesn't code-split, so it wouldn't shrink the bundle). If the delta is unacceptable, - fall back to a precomputed-only provider + `@datadog/flagging-core/precomputed` subpath. (§Step 6, R4) -- **D6 — Security/opt-in:** **no opt-in gate for offline rules** — offline is orthogonal to rules, and the - wire is customer-supplied. The real control is fetch-time API-token scoping in the OnlineProvider - (out of scope). Only a docs caveat is owed. The D5 provider split, if built, is a bundle lever, not a - security gate. (§Step 7, R7) -- **D7 — Obfuscation/hashing:** **upstream concern; assume it works.** Customer pre-hashes context with the - same function used on the rules; the engine does hashed comparisons. RN adds no handling/rejection, only - a normalization-preserves-strings test. (G6, R9) +> **Reviewed over six rounds (2026-07-22 → 07-23); all items confirmed accurate against installed +> `@datadog/flagging-core@2.0.1`, PR #336, native Android/iOS clients, and OpenFeature specs.** +> R1 reversed **D3/D5**, corrected §2 (engine already published), added **G7/G8/D8**, flagged **D6/D7**. +> R2: use the already-published **`evaluateRulesBasedConfiguration`** not `evaluate()` (D4/A); path +> selection **per-resolution** (R12/B); mixed-validity split parse-time vs structural (D); types internal +> (E); regex safety is an **upstream** contract (F); **Q3 narrowed to `extraLogging`** (H); obfuscation +> **undetectable → "unsupported"** (G/D7). R3: **`evaluateRulesBasedConfiguration` DOES return +> `FLAG_NOT_FOUND`** (my R2 note was wrong — corrected) but its lookup is **prototype-unsafe** (G9/bug); +> `ParsedFlagsConfiguration` is **already public** so opacity is only partial (**D10**, R16); the +> **`@openfeature/*` dependency boundary is unsound** (G10, R13); **`id` overrides targeting-key** for +> rule matching (**D9**, R15). R4 (Obfuscation RFC): obfuscation is **salted `ONE_OF_SHA256` operators + +> binary structure**, engine-evaluated and server-compatible — RN no-op — superseding the "customer +> pre-hashes"/"unsupported" framing. R5 (obfuscation deep-dive) tempered R4: those operators are **absent +> from 2.0.1 and today fail *open* to a silent `DEFAULT`** → must **reject unknown operators as `GENERAL`** +> + capability/version (G6); SHA-256 must be **synchronous/Hermes+JSC-safe = new bundle mass** (G11, +> corrects D5); the salted-hash **protocol is unspecified** → needs cross-SDK vectors (G12); "confidential" +> is **overstated** — salt stops precomputation but not offline enumeration of guessable values (D7 threat +> model); and the platform opt-in only covers **Datadog-generated** configs, not customer-supplied wires (D6). +> R6: unsupported-operator validation belongs **upstream** (`validateRulesConfiguration`/capabilities) or +> derived from the pinned `OperatorType` — **not** an RN-maintained set that drifts (G6); unsupported +> operators invalidate the **rules branch**, keeping a valid precomputed sibling (matrix in Step 3); +> malformed **SHA condition shapes** (salt/digest) need load-time rejection (G12); the "what stays visible" +> list must cover **all** UFC metadata incl. `extraLogging`/`doLog`/allocation keys/serialIds/salts (D7); +> "fail-open" → "**silent fallback**". +> **Upstream items (G1, G2, G4, G6, G7, G9, G10, G11, G12) are collaboration points with the Core +> developer** — `@datadog/flagging-core` is under active development, so edge-case gaps/bugs are expected. +> Note them here and work fixes through *together* — **not** external blockers to file or work around +> unilaterally. Raise with Core as the design firms up. + +### Decisions (2026-07-23) +- **D3 — Exposure/`doLog` (REVERSED):** call native `track` for **every successful assignment, + NOT gated on `doLog`** — matching the precomputed path (`FlagsClient.ts:441`, confirmed correct). + Native applies `doLog` to the *exposure event only*; RUM + evaluation telemetry fire independently + (verified in Android `trackResolution` / iOS `trackEvaluation`). Do not track for + disabled/unmatched/type-mismatch/not-found/error (J). **Blocked by G4:** the rules result omits + `extraLogging`, so the synthesized `FlagCacheEntry` is not yet faithful — resolve upstream first. (§Step 5, R3) +- **D4 — Evaluation paths (REFINED):** **keep two paths** — precomputed on the decoded `Map` + `track`; + rules via the **rules-only `evaluateRulesBasedConfiguration()`** (already in 2.0.1), called with the UFC + directly (A). RN selects the path itself (no combined `evaluate()`), so precomputed never routes through + upstream arbitration and `decodePrecomputedFlags` validation is never bypassed. One explicit, + **per-resolution** selection (Step 3/5, B) + a shared mapping helper. (§Step 3/6, R2/R12) +- **D5 — Bundle size (CORRECTED):** the engine + `spark-md5` + the rules evaluator **already ship** via + RN's existing root-barrel import, so enabling rules is a near-zero incremental cost (the wire branch + only). **Two future upstream additions are the real new mass:** the protobuf runtime (G2) and a + **synchronous SHA-256** for obfuscation (G11). **Measure separate baselines.** Dynamic `import()` + rejected (Metro doesn't code-split). A precomputed-only provider + `@datadog/flagging-core/precomputed` + subpath is the only real lever (needs an upstream subpath 2.0.1 lacks + an RN `wire.ts` rework) — pursue + only if measurement demands it. (§Step 6, R4) +- **D6 — Security/opt-in (split the justification by config provenance).** "No additional SDK gate" still + holds, but the *why* differs by source, and the platform argument does **not** cover arbitrary offline + wires: + - **Datadog-generated configs:** the platform enforces distribution policy — a **per-flag switch** + governs whether a flag may be used in rules-based client eval, so the config only carries opted-in + rules. This is the "explicit opt-in" the Offline-Init RFC wants, satisfied server-side. *Caveats:* the + Obfuscation RFC is a first draft (per-flag vs per-org debated), and **scoped client tokens don't exist + today** (RFC:109) — so the fetch-time token control is partly aspirational for the rules case. + - **Customer-supplied / bundled wires (this offline provider accepts these):** they **bypass** Datadog's + generation and token controls entirely, so the platform opt-in doesn't apply. Here the opt-in is simply + **calling `setConfiguration(rules)`**, and the **customer is responsible** for supplying + client-appropriate rules. + Keep product/security sign-off on record; ship the docs caveat (rules are on-device / reverse-engineerable) + and the "what stays visible" list (D7) regardless. (§Step 7, R7) +- **D7 — Obfuscation (design known; NOT yet supportable).** Obfuscation is **new salted operators** + (`ONE_OF_SHA256`/`NOT_ONE_OF_SHA256` from `ONE_OF`, server-compatible, engine-evaluated) + **binary + structure**. It is **not** a separate payload mode and needs **no** RN detection/rejection/context + pre-hashing (the engine hashes the subject attribute with the per-condition salt) — this supersedes the + earlier "customer pre-hashes" / "reject obfuscated" framing. **But three hard upstream prerequisites gate + "supported":** the SHA operators must exist (G6 — today they silently **fall back** to `DEFAULT`), backed by + a **synchronous** Hermes/JSC-safe SHA-256 (G11 — new bundle mass), against a **fully specified portable + hash protocol with cross-SDK test vectors** (G12). RN work: map the operators through once they exist, + **reject unknown operators at load as `GENERAL`** (don't serve a silent default), and verify with the + canonical vectors — not just "strings pass through". + - **Threat model (CORRECTED — do not call this "confidentiality"):** a public per-condition salt + one + fast SHA-256 defeats **cross-config precomputation / rainbow tables** and **obscures literal values**, + but does **not** protect **low-entropy / guessable** values — anyone with the bundled config can hash + candidates (`true`/`off`, common plans, domains, email dictionaries) against the included salt (NIST + SP 800-132: known-salt lets an attacker enumerate likely candidates; only added work slows dictionary + attacks). State it precisely; do not claim guessable values are hidden. + - **What stays visible** (document for customers — Step 8; **all** shipped UFC data per `ufc-v1.d.ts`): + flag/variant/attribute **names**; variant **values**; regex/numeric/version operands; decodable config + **structure**; **guessable hashed membership values**; **plus** allocation **keys**, split + **serialIds**, **`extraLogging`**, **`doLog`**, allocation `startAt`/`endAt`, shard **salts**, + **environment name**, **`createdAt`**. (G6/G11/G12, R9) +- **D8 — Targeting key (NEW):** decide whether missing (`undefined`) and anonymous-empty (`''`) are + distinct. `''` buckets as a real subject; only null/undefined yields `TARGETING_KEY_MISSING`. If + distinct, thread `undefined` through the rules path and add `TARGETING_KEY_MISSING` to `FlagErrorCode`; + if not, document that all keyless contexts share one bucket. (§Step 4/7, G8, R11) +- **D9 — `id` vs `targetingKey` (NEW):** the engine lets a customer `id` attribute override the + targeting-key id for **rule matching** while **sharding** uses the targeting key. **Recommended:** + reserve `id` for the targeting key — drop a customer-supplied `id` in the flat adapter so matching and + bucketing share one subject id. Assert in tests. (§Step 5, R15) +- **D10 — Config opacity (NEW):** `ParsedFlagsConfiguration` is already public and structurally exposes + `precomputed` (today) and `rulesBased` (post-bump). Choose: **(a) accept structural visibility and drop + the "opaque" language** (pragmatic — matches how precomputed already ships), or **(b) brand + `ParsedFlagsConfiguration`** as opaque (breaking type-compat change). Not exporting the sub-types is + necessary but **not sufficient** for opacity. (§Step 1/8, R16) +- **D11 — OpenFeature dependency boundary (NEW):** do not import `@openfeature/*` bare into `packages/core`. + Choose: **(a)** keep OF types in react-native-openfeature and pass structurally-compatible internal + context/logger types into `FlagsClient` (no core dep — preferred, and keeps threading off the public + `get*Details` API via an internal entry point), or **(b)** add an explicit core `@openfeature/*` dep and + require flagging-core to declare `@openfeature/core` as a real (non-dev) dependency. (§Step 5, G10, R13) ### Remaining open questions (PUNTED — revisit with flagging-core owners; do not block planning) -- [ ] **Q1 (punted):** which published `@datadog/flagging-core` version carries rules; confirm root exports - (`evaluate`, `UniversalFlagConfigurationV1`, `RulesBasedConfiguration`) and the final wire field - name/version. (G1/G3/R8) +- [ ] **Q1 (punted):** which published `@datadog/flagging-core` version adds the `rulesBased` wire parsing + + parsed-config slot; confirm `configurationFromString` populates the rules branch and the final wire + field name/version. (`evaluateRulesBasedConfiguration` + `UniversalFlagConfigurationV1` already ship.) (G1/G3/R8) - [ ] **Q2 (punted):** protobuf rules encoding — when flagging-core publishes the `.proto` and switches - `response` from JSON to protobuf/base64-decode; which protobuf runtime, and is it Hermes-safe? (G2/§2.5) + `response` from JSON to protobuf/base64-decode; which protobuf runtime, and is it Hermes-safe. (G2/§2.5) +- [ ] **Q3 (narrowed):** will upstream expose **`extraLogging`** on the rules-eval result (serialId + + timestamp are already in `flagMetadata`, and RN's bridge has no slot for them anyway), and does native + telemetry need the original `INTEGER`/`NUMERIC` type (engine collapses both to `number`)? (G4/D3 — blocking) From 1ca1e42817868e3558b74d62f2952f6b042ef078 Mon Sep 17 00:00:00 2001 From: Blake Date: Thu, 23 Jul 2026 09:55:49 -0400 Subject: [PATCH 5/8] docs(flags): add simplified dynamic offline plan --- dynamic_offline_simplified.plan.md | 1081 ++++++++++++++++++++++++++++ 1 file changed, 1081 insertions(+) create mode 100644 dynamic_offline_simplified.plan.md diff --git a/dynamic_offline_simplified.plan.md b/dynamic_offline_simplified.plan.md new file mode 100644 index 000000000..699f420b2 --- /dev/null +++ b/dynamic_offline_simplified.plan.md @@ -0,0 +1,1081 @@ +# FFL-2837 — Dynamic Offline Initialization for React Native + +This document uses Simplified Technical English. +Technical names and API names do not change. + +**Jira:** FFL-2837 +**Base branch:** `blake.thomas/FFL-2666` +**Work branch:** `blake.thomas/FFL-2837` +**Upstream reference:** DataDog/openfeature-js-client PR #336 + +## Source documents + +- The Portable Flag Configuration RFC defines the portable configuration APIs. +- The Offline Initialization RFC defines the offline workflows. +- The ConfigurationWire specification defines the intended protobuf and base64 format. +- The Obfuscation RFC defines the proposed client-rules protection. + +These documents are drafts. +Names, versions, and formats can change. + +## 1. Objective + +The current offline provider supports precomputed assignments. +A precomputed configuration applies to one evaluation context. +A different context puts the provider in the `ERROR` state. + +Add a rules-based offline flow. + +- The customer supplies a rules configuration with `setConfiguration`. +- The provider does not fetch data from the network. +- `setContext` changes the active context. +- The SDK evaluates the rules locally for each flag request. +- A rules configuration can evaluate more than one context. +- The SDK uses `evaluateRulesBasedConfiguration` from `@datadog/flagging-core`. +- The SDK does not implement a second rules engine. + +One provider supports both configuration types. +A wire can contain precomputed data, rules data, or both. + +Use this evaluation order: + +1. Use precomputed data when its context matches. +2. Otherwise, use rules data when it is usable. +3. Otherwise, return a configuration error. + +Do not add a new SDK mode. +Do not add an `offlineInit` method. +Use this existing flow: + +```text +configurationFromString -> setConfiguration -> evaluate +``` + +## 2. Current `@datadog/flagging-core` state + +### 2.1 Features in version 2.0.1 + +Version 2.0.1 already contains the rules engine. +It exports the engine from the package root. + +It contains these parts: + +- `evaluateForSubject` +- `evaluateRulesBasedConfiguration` +- Rule operators and rule matching +- Sharding and hashing +- UFC v1 types +- `TargetingKeyMissingError` +- Evaluation metadata +- MD5 utility functions +- The `spark-md5` dependency + +Version 2.0.1 has these configuration limits: + +- `FlagsConfiguration` contains only `precomputed`. +- The wire parser reads only precomputed data. +- The package has one root export. +- The package has no subpath exports. + +The React Native wire module already imports the package root. +Metro does not remove the unused rules engine. +Thus, the current application bundle already contains the rules engine and `spark-md5`. + +### 2.2 Features in upstream PR #336 + +PR #336 adds these core features: + +- A `rulesBased` wire branch +- A `rulesBased` field in `FlagsConfiguration` +- A combined `evaluate` function + +React Native does not need the combined `evaluate` function. +React Native selects the evaluation path itself. +React Native calls `evaluateRulesBasedConfiguration` for the rules path. + +The required dependency update must provide these two features: + +- Rules wire parsing +- The parsed `rulesBased` field + +PR #336 is open. +Its merge state is dirty. +It requires review. + +### 2.3 Browser provider behavior + +Use the browser `CoreProvider` as a reference. + +- A rules configuration is valid for any context. +- A precomputed mismatch is an error only when no rules fallback exists. +- `onContextChange` stores the new context. +- `onContextChange` does not fetch. +- `setConfiguration` emits the applicable provider event. + +### 2.4 Evaluation path + +Select the path for each resolution. +Do not select the path only during reconciliation. + +Use this order: + +1. If precomputed data matches the effective context, use its decoded `Map`. +2. Otherwise, if valid rules data exists, evaluate the rules. +3. Otherwise, if precomputed data exists, return `INVALID_CONTEXT`. +4. Otherwise, return `PROVIDER_NOT_READY`. + +The rules evaluator converts `targetingKey` to the `id` subject attribute. +It does this only when `targetingKey` is not null. + +The evaluator returns `ResolutionDetails`. +The metadata contains these applicable fields: + +- `allocationKey` +- `variationType` +- `doLog` +- `__dd_split_serial_id` +- `__dd_allocation_key` +- `__dd_do_log` +- `__dd_eval_timestamp_ms` + +The metadata does not contain `extraLogging`. +This missing field blocks correct native tracking. + +The evaluator treats an empty targeting key as a real subject. +It raises `TARGETING_KEY_MISSING` only for a null or undefined key. + +The React Native `EvaluationContext` requires a string key. +Its documentation tells customers to use an empty string. +The React Native `FlagErrorCode` does not contain `TARGETING_KEY_MISSING`. + +## 2.5 Wire format + +The source documents use different wire formats. + +- The ConfigurationWire specification uses version `1`, field `rules`, and protobuf/base64. +- PR #336 uses version `1`, field `rulesBased`, and JSON. +- The Portable Flag Configuration RFC uses version `2`, field `server`, and JSON. + +The intended rules response uses protobuf and base64. +The current upstream code uses JSON. +Do not depend on the JSON format. + +The Obfuscation RFC requests a binary format. +This request does not select protobuf by itself. +The ConfigurationWire decision selects protobuf. + +The protobuf schema must support these new operators: + +- `ONE_OF_SHA256` +- `NOT_ONE_OF_SHA256` + +The schema must also support the salt fields. + +Keep all decoding in `@datadog/flagging-core`. +Do not add a rules decoder to React Native. + +A future protobuf runtime will add bundle size. +A future synchronous SHA-256 implementation can also add bundle size. +Test both additions with Hermes and JSC. + +Publish the `.proto` schema before the protobuf migration. +Pin React Native to a released flagging-core version. +Verify the final field names and versions after publication. +Do not hard-code the wire field name in React Native. + +## 3. Upstream gaps + +### G1 — Rules wire parsing and parsed field + +**Status:** Blocking. + +Merge and publish the upstream rules wire changes. +Bump `@datadog/flagging-core` in `packages/core/package.json`. +Do not add the dependency to `packages/react-native-openfeature`. + +Use a linked package or an `npm pack` package during development. + +### G2 — Protobuf rules response + +**Status:** Upstream release contract. + +Publish the `.proto` schema. +Change the rules parser from JSON to protobuf. +Test the protobuf runtime with Hermes and JSC. + +React Native must continue to use the opaque parser. + +### G3 — Root exports + +**Status:** Mostly complete. + +Version 2.0.1 exports these required symbols: + +- `UniversalFlagConfigurationV1` +- `evaluateRulesBasedConfiguration` + +Verify that the new release populates the `rulesBased` field. + +### G4 — Native tracking metadata + +**Status:** Blocking. + +The evaluator already returns the split serial ID and evaluation timestamp. +React Native cannot send those fields through its current bridge. + +The evaluator does not return `extraLogging`. +React Native needs this field to build `FlagCacheEntry`. + +Define the exact native tracking payload. +Then, select one upstream solution: + +- Add `extraLogging` to the rules result. +- Add a rules-tracking API. + +Also verify the required variation type. +The evaluator converts `INTEGER` and `NUMERIC` to `number`. +The precomputed path keeps the original distinction. + +### G5 — Bundle size and JavaScript engine support + +**Status:** Low for current code. + +The rules engine and `spark-md5` already ship in the bundle. +The rules wire branch adds little current bundle size. + +Two future changes can add significant code: + +- The protobuf runtime +- A synchronous SHA-256 implementation + +Measure each change. +Test each change with Hermes and JSC. + +Do not use a dynamic import. +Metro does not create a smaller release bundle from this import. + +Consider a precomputed-only package split only after measurement. +This split requires a flagging-core subpath export. + +### G6 — Unsupported obfuscation operators + +**Status:** Blocking. + +Version 2.0.1 does not support the SHA-256 operators. +An unknown operator makes the current evaluator return `DEFAULT`. +The evaluator does not return an error. +This silent fallback can return the wrong value. + +Do not maintain a separate operator list in React Native. +Use one of these preferred upstream solutions: + +1. Export `validateRulesConfiguration`. +2. Export evaluator capabilities. +3. Return `GENERAL` for an unsupported operator. +4. Reject the operator in the parser. + +If React Native needs a temporary check, use the pinned `OperatorType` enum. +Do not create a second list. + +Define capability ownership. + +- An official fetch request must advertise evaluator capabilities. +- The service must omit or reject unsupported flags. +- A portable wire must state its required capabilities. +- Alternatively, the parser must preserve and reject unknown operators. + +An unsupported operator invalidates the rules branch. +It does not always invalidate a valid precomputed branch. + +Use this state matrix: + +- Rules only and unsupported operator: return `GENERAL`. +- Matching precomputed data and unsupported rules: serve precomputed data. +- Mismatched precomputed data and unsupported rules: return `GENERAL`. +- Hook context falls through to unsupported rules: return `GENERAL` for that resolution. + +Keep the provider `READY` while matching precomputed data is usable. +Invalidate the complete rules branch for one unsupported operator. +Do not isolate an unsupported operator to one flag in this release. + +### G7 — Untrusted rules and regular expressions + +**Status:** High risk. + +The evaluator reads `config.flags[flagKey]` before its `try` block. +A malformed UFC can throw an exception. + +The rules engine creates regular expressions from wire values. +A hostile expression can block the JavaScript thread. + +Validate the rules snapshot before storage. +Validate the envelope, flags, allocations, splits, variations, and shard ranges. +Validate reserved property names. + +Do not claim that structural validation stops ReDoS. +Select one regular-expression protection: + +- An upstream safe-regex guarantee +- A static safe-regex policy +- A bounded regular-expression engine + +Run hostile-expression tests in a separate process. +Set a time limit for that process. + +Clone the rules snapshot before you freeze it. +Do not freeze the caller's object. + +### G8 — Missing targeting key + +**Status:** Decision required. + +Decide if a missing key and an empty key are different. + +If they are different: + +- Preserve `undefined` for the rules path. +- Relax the internal context type. +- Add `TARGETING_KEY_MISSING` to `FlagErrorCode`. + +If they are not different: + +- Use the empty string. +- Document that all keyless contexts use one subject bucket. + +### G9 — Prototype-unsafe flag lookup + +**Status:** Bug. + +The evaluator uses `config.flags[flagKey]`. +A missing reserved key can resolve through `Object.prototype`. + +Examples include: + +- `toString` +- `__proto__` +- `constructor` + +The evaluator can return `DISABLED` instead of `FLAG_NOT_FOUND`. + +Use an own-property check before evaluation. +Alternatively, fix the lookup in flagging-core. +A null-prototype dictionary is also acceptable. + +The precomputed path already uses a `Map`. + +### G10 — OpenFeature type dependency + +**Status:** Decision required. + +`packages/core` has no OpenFeature dependency. +The flagging-core declaration files import `@openfeature/core`. +Flagging-core lists that package only as a development dependency. +Local hoisting hides this problem. + +Select one solution: + +1. Keep OpenFeature types in `react-native-openfeature`. +2. Pass compatible internal context and logger types to core. +3. Or, add an explicit core dependency. +4. If you add the dependency, fix the flagging-core package dependency too. + +Do not widen the public `FlagsClient.get*Details` methods. +Prefer a separate internal entry point. + +### G11 — Synchronous SHA-256 + +**Status:** Blocking upstream work. + +Flag evaluation is synchronous. +Web Crypto `SubtleCrypto.digest` is asynchronous. +Do not use it in the synchronous evaluation path. + +Use a synchronous SHA-256 implementation. +The implementation must work with Hermes and JSC. +It must work across the supported React Native versions. + +Do not depend on Node `crypto`. +Do not depend on a browser-only global. +Do not depend on an unavailable runtime global. + +Measure bundle size and evaluation time in release builds. + +### G12 — Portable salted-hash protocol + +**Status:** Blocking upstream contract. + +The Obfuscation RFC does not fully define the hash protocol. +Define these items: + +- Salt length +- Salt encoding +- Salt and value order +- Input framing +- UTF-8 encoding +- Unicode normalization +- Digest encoding +- Hexadecimal letter case +- Base64 padding, if applicable +- Number conversion +- Boolean conversion +- Empty-string behavior +- Null and missing-attribute behavior +- `NOT_ONE_OF_SHA256` behavior + +Publish canonical cross-SDK test vectors. +The generator and all evaluators must produce the same bytes. + +Validate each SHA condition before the provider becomes `READY`. +Reject these conditions: + +- Missing salt +- Malformed salt +- Excessively large salt +- Incorrect salt length +- Incorrect salt encoding +- Incorrect digest length +- Incorrect digest encoding +- Non-string digest values +- Malformed condition shape +- Excessive condition count +- Excessive value count + +## 4. React Native implementation + +### Step 0 — Complete prerequisites + +- [ ] Publish flagging-core with rules wire parsing. +- [ ] Publish the parsed `rulesBased` field. +- [ ] Publish the required SHA operators before obfuscation support. +- [ ] Define and publish the salted-hash protocol. +- [ ] Provide a synchronous SHA-256 implementation. +- [ ] Resolve the `extraLogging` tracking requirement. +- [ ] Bump `@datadog/flagging-core` in `packages/core`. +- [ ] Update `yarn.lock`. +- [ ] Verify all final field names, versions, and exports. +- [ ] Record the exact post-SHA flagging-core version. + +### Step 1 — Define the parsed configuration type + +Do not export a named rules or UFC type. +Use `UniversalFlagConfigurationV1` only inside the flags implementation. + +`ParsedFlagsConfiguration` is already public. +It is an alias of upstream `FlagsConfiguration`. +The new upstream type will expose `rulesBased.response`. + +Select one API policy: + +1. Accept structural visibility and remove claims of opacity. +2. Make `ParsedFlagsConfiguration` a branded type. + +The second policy is a breaking type change. +Do not claim that the type is opaque unless you enforce opacity. + +### Step 2 — Use the upstream wire parser + +Do not add React Native parsing code. +Re-export the upstream conversion functions. + +Add a round-trip test for a rules wire. +Use the encoding from the pinned upstream version. + +### Step 3 — Load and validate the configuration + +Keep the complete parsed `FlagsConfiguration`. +Decode precomputed flags one time into a `Map`. +Keep a validated rules snapshot. + +Use this path order: + +1. Matching precomputed data +2. Valid rules data +3. Error + +Select the path for each resolution. +Do not freeze the selection during reconciliation. + +Treat parse failures and validation failures differently. + +For a parse failure: + +- The current parser returns an empty object. +- React Native cannot recover the valid sibling branch. +- Return `GENERAL`. + +For a decoded validation failure: + +- Validate each branch separately. +- Keep a valid sibling branch. +- Mark the invalid branch as unusable. +- Do not use the invalid branch as a fallback. + +Validate rules before the precomputed-only guard. +Do not store an unvalidated UFC. + +Use the G6 state matrix for unsupported operators. +Use upstream validation when it is available. +If necessary, derive temporary validation from pinned `OperatorType`. + +Validate all SHA condition fields. +Apply the size limits from G12. + +### Step 4 — Reconcile the context + +For valid rules, accept every external context. +Set `configurationStatus` to `ready`. +Do not fill `flagsCache` for rules. + +Do not create an empty targeting key until D8 is complete. + +Keep the precomputed behavior. +Return `INVALID_CONTEXT` only when no usable rules fallback exists. + +### Step 5 — Evaluate a flag + +Call the rules-only evaluator: + +```ts +const details = evaluateRulesBasedConfiguration( + rulesResponse, + type, + key, + defaultValue, + ofContext, + logger +); +``` + +Convert the React Native context to a flat OpenFeature context. +Pass the effective resolution context. +Pass the resolution logger. + +The web SDK uses the static-context model. +It has no invocation context. +The effective context comes from global or domain state. +A `before` hook can change this context for one resolution. + +Check the precomputed context for every resolution. +This check prevents an assignment leak after a hook changes context. + +Resolve the OpenFeature dependency boundary before implementation. +Prefer compatible internal types and an internal `FlagsClient` method. + +Decide the `id` policy. +The current evaluator lets a custom `id` replace the targeting-key `id`. +Sharding still uses `targetingKey`. +This can use two subject identifiers. + +The recommended policy reserves `id` for `targetingKey`. +Drop or reject a customer `id` attribute. +Test that targeting and sharding use the same identifier. + +Map all evaluator results to `FlagDetails`. +The evaluator already returns `FLAG_NOT_FOUND`. +Do not create this error again in React Native. + +Add an own-property check for the flag key. +Return `FLAG_NOT_FOUND` for an absent reserved-name key. + +Track every successful assignment through the native bridge. +Do not use `doLog` to stop the bridge call. +Native code uses `doLog` only for the exposure event. +RUM and evaluation telemetry use separate settings. + +Track only a real assigned variant. +Do not track these results: + +- `DISABLED` +- Unmatched `DEFAULT` +- No-variant `DEFAULT` +- `TYPE_MISMATCH` +- `FLAG_NOT_FOUND` +- Error results + +Do not implement tracking until G4 is complete. + +Keep the online cache path unchanged. +Keep the precomputed cache path unchanged. + +### Step 6 — Support configurations with two branches + +Use matching precomputed data first. +Use rules only when precomputed data is absent or mismatched. +Call the rules evaluator with only the UFC. + +Do not pass precomputed data to the rules evaluator. +Do not bypass `decodePrecomputedFlags`. + +Create one shared result-mapping helper. +Use it to build `FlagDetails` and `FlagCacheEntry`. + +Measure these bundle baselines separately: + +1. Current baseline +2. Root SDK import +3. Online flags +4. Precomputed offline flags +5. Dynamic offline flags +6. Post-protobuf dependency +7. Post-SHA dependency + +Do not add a dynamic import. +Consider a precomputed-only split only when measurements require it. + +### Step 7 — Update the offline provider + +Keep `initialize` network-free. +Keep `onContextChange` network-free. + +For valid rules, reconciliation returns `ready`. +Do not use the precomputed mismatch error for valid rules. + +Resolve D8 before you define empty-context behavior. + +Update the class comment. +Remove the precomputed-only instruction that forbids `setContext`. +Explain that `setContext` is required for dynamic rules. + +Keep the existing provider event mapping. +Test all transitions. + +Do not add a separate SDK gate. +Use different opt-in explanations for each configuration source. + +For a Datadog-generated configuration: + +- The platform distribution control is the opt-in. +- The Obfuscation RFC is still a first draft. +- Scoped client tokens do not exist yet. + +For a customer-supplied configuration: + +- Platform controls do not apply. +- `setConfiguration(rules)` is the opt-in. +- The customer must supply client-appropriate rules. + +Record product and security approval. + +### Step 8 — Update exports, examples, and documentation + +Do not export a named rules or UFC type. +Complete the D10 opacity decision. + +Rewrite the offline README section. +Show the precomputed flow and the rules flow separately. +Apply each warning only to the applicable flow. + +Add a rules example to both example applications. +Show two calls to `setContext`. +Show that the values can change. + +Update the provider class comment. + +Document all data that remains visible: + +- Flag names +- Variant names +- Attribute names +- Variant values +- Regex operands +- Numeric operands +- Version operands +- Decodable configuration structure +- Allocation keys +- Split serial IDs +- `doLog` +- `extraLogging` +- Environment metadata +- Timestamps +- Salts +- Digests +- Guessable hashed membership values + +Do not call this data confidential. +A public salt prevents reusable precomputation. +It does not stop offline guessing of low-entropy values. + +## 5. Imports and internal interfaces + +Add this value import from `@datadog/flagging-core`: + +- `evaluateRulesBasedConfiguration` + +Add this internal type import: + +- `UniversalFlagConfigurationV1` + +Keep the existing wire and precomputed imports. + +Do not import OpenFeature types directly into core unless D11 selects that policy. +Prefer compatible internal context and logger interfaces. +Use an internal `FlagsClient` entry point. + +Do not export `ParsedRulesBasedConfiguration`. +Do not export `UniversalFlagConfigurationV1`. + +Add one internal context adapter. +Convert between these forms: + +```text +React Native: { targetingKey, attributes } +OpenFeature: { targetingKey, ...attributes } +``` + +No new native API is required. +The existing tracking bridge accepts a synthesized flag object. +G4 must supply the missing tracking metadata. + +## 6. Test plan + +### 6.1 Wire and configuration tests + +- [ ] Parse a rules wire into `rulesBased.response`. +- [ ] Serialize the parsed rules configuration. +- [ ] Parse a wire with both branches. +- [ ] Return an empty configuration for malformed wire data. +- [ ] Return `GENERAL` when the loaded configuration is empty. +- [ ] Detect a changed upstream field name or version. +- [ ] Test the protobuf format after it becomes available. +- [ ] Verify unknown protobuf enum behavior. + +### 6.2 Load and reconciliation tests + +- [ ] Load rules only and reach `ready` with an empty context. +- [ ] Change context without a native fetch. +- [ ] Reset context without a native fetch. +- [ ] Return `PROVIDER_NOT_READY` when no configuration exists. +- [ ] Use matching precomputed data before rules. +- [ ] Use rules after a precomputed mismatch. +- [ ] Return `INVALID_CONTEXT` for a precomputed-only mismatch. +- [ ] Apply the unsupported-operator state matrix. +- [ ] Keep valid precomputed data when the rules branch is invalid. + +### 6.3 Rules evaluation tests + +- [ ] Evaluate boolean, string, number, and object flags. +- [ ] Return different values for contexts in different buckets. +- [ ] Return `FLAG_NOT_FOUND` for an absent flag. +- [ ] Return `TYPE_MISMATCH` for the wrong resolver. +- [ ] Return `FLAG_NOT_FOUND` for absent reserved-name keys. +- [ ] Apply the selected `id` policy. +- [ ] Return `DISABLED` for a disabled flag. +- [ ] Return `DEFAULT` when no allocation matches. +- [ ] Return `DEFAULT` when no variant exists. +- [ ] Test allocations before `startAt`. +- [ ] Test allocations at or after `endAt`. +- [ ] Test the selected missing-targeting-key policy. +- [ ] Treat an empty string as a real targeting key. + +### 6.4 Per-resolution context tests + +- [ ] Load matching precomputed data and rules data. +- [ ] Change one resolution context with a `before` hook. +- [ ] Confirm that the resolution does not use mismatched precomputed data. +- [ ] Confirm that the resolution uses rules or returns the applicable error. + +### 6.5 Tracking tests + +- [ ] Call native tracking for every successful assignment. +- [ ] Call native tracking when `doLog` is false. +- [ ] Include the variation key. +- [ ] Include the allocation key. +- [ ] Include the string variation value. +- [ ] Include `extraLogging`. +- [ ] Do not track `DISABLED`. +- [ ] Do not track unmatched `DEFAULT`. +- [ ] Do not track `FLAG_NOT_FOUND`. +- [ ] Do not track `TYPE_MISMATCH`. +- [ ] Do not track error results. + +### 6.6 Validation and security tests + +- [ ] Reject a missing `flags` map. +- [ ] Reject a malformed flag. +- [ ] Reject a bad shard range. +- [ ] Test inherited property names. +- [ ] Test mutation of the source object after load. +- [ ] Clone the source object before freezing. +- [ ] Run a hostile regex in an isolated process. +- [ ] Stop the hostile-regex process at its time limit. +- [ ] Verify the selected ReDoS protection. +- [ ] Reject an unsupported operator as `GENERAL`. +- [ ] Do not return a silent `DEFAULT` for an unsupported operator. +- [ ] Test a newer cached configuration with an older evaluator. +- [ ] Reject malformed SHA salts and digests. +- [ ] Reject oversized SHA conditions. + +### 6.7 Mixed-configuration tests + +- [ ] Confirm that one parse failure currently removes both branches. +- [ ] Confirm that React Native cannot recover a sibling after this parse failure. +- [ ] Isolate a decoded structural failure to its branch. +- [ ] Keep a valid sibling branch. +- [ ] Return `GENERAL` when the active path uses an invalid rules branch. + +### 6.8 Obfuscation tests + +- [ ] Preserve string attributes through context processing. +- [ ] Use the canonical SHA test vectors. +- [ ] Test number conversion. +- [ ] Test boolean conversion. +- [ ] Test an empty string. +- [ ] Test null and missing attributes. +- [ ] Test `NOT_ONE_OF_SHA256`. +- [ ] Confirm byte-identical results across SDK implementations. + +### 6.9 Provider tests + +- [ ] Load rules before provider registration. +- [ ] Reach `READY`. +- [ ] Change from context A to context B. +- [ ] Confirm that no fetch occurs. +- [ ] Confirm that the evaluated value changes. +- [ ] Do not require a `RECONCILING` event. +- [ ] Emit `CONFIGURATION_CHANGED` for valid replacement data. +- [ ] Emit `READY` when valid data recovers an error. +- [ ] Emit `PROVIDER_ERROR` for invalid data. +- [ ] Return the same result for the same context and configuration. +- [ ] Keep all precomputed regression tests. + +### 6.10 Integration and performance tests + +- [ ] Parse a real rules wire. +- [ ] Evaluate several flags and contexts. +- [ ] Verify native tracking calls. +- [ ] Use a service or flagging-core fixture. +- [ ] Measure all bundle baselines from Step 6. +- [ ] Measure the protobuf addition separately. +- [ ] Measure the synchronous SHA addition separately. +- [ ] Run rules evaluation with Hermes. +- [ ] Run rules evaluation with JSC. +- [ ] Test the supported React Native version range. +- [ ] Confirm that no Node or browser-only crypto API is required. +- [ ] Measure repeated rules evaluation in a release build. + +## 7. Risks + +### R1 — Unpublished upstream configuration support + +PR #336 is not merged. +Its API can change. +Keep the React Native integration small. +Use one dependency update as the integration point. + +### R2 — Two evaluation paths + +Precomputed data uses a `Map`. +Rules data uses the rules evaluator. +Reason codes and type checks can differ. +Use one result-mapping helper. +Test the selection order. + +### R3 — Tracking parity + +The rules result does not contain `extraLogging`. +Do not ship incomplete tracking. +Resolve G4 first. + +### R4 — Bundle size + +The current engine already ships. +Protobuf and synchronous SHA-256 are future additions. +Measure both additions. + +### R5 — Hermes and JSC support + +Test sharding, MD5, protobuf, and SHA-256. +Test release builds across the supported React Native range. + +### R6 — Context and logger transfer + +The provider currently discards its context and logger parameters. +Transfer the effective values through a safe internal boundary. +Resolve D9 and D11 first. + +### R7 — Opt-in and configuration source + +Platform controls apply only to Datadog-generated configurations. +Customer-supplied configurations bypass those controls. +Document both cases. + +### R8 — Wire changes + +The source documents use different names and versions. +Pin the released dependency. +Add a contract test. + +### R9 — Obfuscation + +The design is not supportable until G6, G11, and G12 are complete. +Do not describe salted SHA-256 as confidentiality. +It does not protect guessable values from offline enumeration. + +### R10 — Untrusted input and ReDoS + +Malformed rules can throw. +Hostile regex data can block the JavaScript thread. +Validate the snapshot and select a regex protection. + +### R11 — Missing targeting key + +The current React Native type cannot represent a missing key. +Complete D8 before you implement the adapter. + +### R12 — Per-resolution path selection + +A hook can change one resolution context. +Check precomputed compatibility after this change. + +### R13 — OpenFeature dependency + +Current type resolution depends on workspace hoisting. +Complete D11 before you transfer OpenFeature types into core. + +### R14 — Prototype lookup + +Reserved-name keys can return the wrong result. +Use an own-property check or an upstream fix. + +### R15 — Subject identifier + +Rule matching and sharding can use different identifiers. +Complete D9 before implementation. + +### R16 — Public configuration type + +The public alias exposes the upstream structure. +Complete D10 before you claim that the type is opaque. + +## 8. Decisions + +### D3 — Native tracking + +Call native tracking for every successful assignment. +Do not stop the bridge call when `doLog` is false. +Do not track default or error results. +Complete G4 before implementation. + +### D4 — Evaluation paths + +Keep two paths. +Use the decoded `Map` for precomputed data. +Use `evaluateRulesBasedConfiguration` for rules data. +Select the path for each resolution. + +### D5 — Bundle size + +The current rules engine already ships. +Measure protobuf and synchronous SHA-256 as separate future additions. +Do not use dynamic import as a size control. + +### D6 — Security opt-in + +Do not add an additional provider gate. + +For Datadog-generated data, use the platform distribution policy. +For customer-supplied data, treat `setConfiguration` as the opt-in. +Document that the customer must supply client-appropriate rules. + +### D7 — Obfuscation + +The design uses salted SHA-256 membership operators and binary structure. +It does not use a separate obfuscated payload mode. +React Native does not pre-hash customer context. + +Do not claim support until G6, G11, and G12 are complete. +Reject unsupported operators as `GENERAL`. +Use canonical test vectors. + +A public salt stops reusable precomputation. +It does not stop guesses of low-entropy values. +Document all visible UFC data. + +### D8 — Targeting key + +**Status:** Open. + +Decide whether missing and empty keys are different. +Then, update the types, error codes, adapter, and documentation. + +### D9 — `id` and `targetingKey` + +**Recommended decision:** Reserve `id` for `targetingKey`. + +Drop a customer `id` attribute in the flat adapter. +Make rule matching and sharding use one subject identifier. + +### D10 — Configuration opacity + +**Status:** Open. + +Select one policy: + +1. Accept the public structural type. +2. Introduce a branded opaque type. + +The branded type can break type compatibility. + +### D11 — OpenFeature dependency boundary + +**Preferred decision:** Keep OpenFeature types out of core. + +Use compatible internal context and logger types. +Use an internal `FlagsClient` entry point. + +Alternative: + +- Add an explicit core OpenFeature dependency. +- Fix the flagging-core published dependency. + +## 9. Open questions + +### Q1 — Published flagging-core version + +- [ ] Identify the version that contains rules wire parsing. +- [ ] Confirm the final rules field name. +- [ ] Confirm the final wire version. +- [ ] Confirm that `configurationFromString` populates the rules branch. + +### Q2 — Protobuf implementation + +- [ ] Publish the `.proto` schema. +- [ ] Define the protobuf runtime. +- [ ] Confirm Hermes compatibility. +- [ ] Confirm JSC compatibility. +- [ ] Define unknown-enum behavior. +- [ ] Add SHA operators and salt fields to the schema. + +### Q3 — Tracking metadata + +- [ ] Decide how rules evaluation returns `extraLogging`. +- [ ] Confirm whether native telemetry needs the original UFC numeric type. + +## 10. Review history + +The plan had six review rounds on 2026-07-22 and 2026-07-23. +The reviews checked local version 2.0.1, PR #336, native tracking, and OpenFeature behavior. + +The reviews produced these main corrections: + +- The rules engine already exists in version 2.0.1. +- React Native must call the rules-only evaluator. +- Path selection must occur for each resolution. +- Native tracking must cross the bridge for every successful assignment. +- The engine already adds bundle size today. +- Protobuf and synchronous SHA-256 add future bundle size. +- Rules validation must protect against malformed input and ReDoS. +- A missing targeting key differs from an empty string in the evaluator. +- The evaluator flag lookup is not safe for prototype names. +- The public parsed configuration type is not fully opaque. +- OpenFeature types currently resolve through hoisting. +- Custom `id` can conflict with `targetingKey`. +- Unsupported operators currently cause a silent `DEFAULT`. +- Operator validation should belong to flagging-core. +- Unsupported rules must not remove a valid precomputed branch. +- The salted-hash protocol needs canonical cross-SDK test vectors. +- Salted SHA-256 does not make low-entropy values confidential. +- Platform opt-in does not apply to customer-supplied wires. + +Coordinate upstream work with the flagging-core developers. +Do not implement an incompatible local rules engine. From 7e61465b8538d79248ee2b9407c292f2aa29d6ad Mon Sep 17 00:00:00 2001 From: Blake Date: Thu, 23 Jul 2026 20:38:18 -0400 Subject: [PATCH 6/8] docs(flags): define dynamic offline PR stack --- dynamic_offline_pr_stack.plan.md | 102 +++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 dynamic_offline_pr_stack.plan.md diff --git a/dynamic_offline_pr_stack.plan.md b/dynamic_offline_pr_stack.plan.md new file mode 100644 index 000000000..ae3ab3491 --- /dev/null +++ b/dynamic_offline_pr_stack.plan.md @@ -0,0 +1,102 @@ +# FFL-2837 — Dynamic Offline PR Stack + +This document uses Simplified Technical English. + +## Stack + +Use these branches: + +1. `blake.thomas/FFL-2837-PR1` +2. `blake.thomas/FFL-2837-PR2` +3. `blake.thomas/FFL-2837-PR3` + +PR1 uses `blake.thomas/FFL-2837` as its base. +PR2 uses `blake.thomas/FFL-2837-PR1` as its base. +PR3 uses `blake.thomas/FFL-2837-PR2` as its base. + +Keep all three pull requests in draft state. + +## Temporary upstream code + +The published flagging-core package does not contain the final rules wire contract. +It also does not contain all required validation and tracking metadata. + +Put a `TODO` immediately before each temporary implementation. +The `TODO` must identify the upstream replacement. +Do not hide temporary behavior in a general helper. +Tests can use a fake rules engine. +Production code must use one internal engine adapter. + +## PR1 — Rules configuration and engine boundary + +Add the internal boundary for the rules engine. + +- Add internal rules configuration types. +- Add a rules-engine adapter. +- Convert SDK contexts to engine contexts. +- Normalize engine results. +- Validate rules before storage. +- Clone valid rules before storage. +- Add adapter contract tests. +- Add fake-engine test helpers. +- Keep current provider behavior unchanged. +- Keep precomputed evaluation unchanged. + +The main review question is: + +> Does this boundary isolate the SDK from the upstream rules engine? + +## PR2 — Core dynamic and mixed evaluation + +Add dynamic evaluation to `FlagsClient`. + +- Store precomputed and rules branches independently. +- Keep a valid branch when its sibling is invalid. +- Reconcile a rules branch as ready for each context. +- Select the evaluation path for each resolution. +- Use matching precomputed data first. +- Use valid rules data second. +- Return the applicable error when neither path is usable. +- Map rules results to `FlagDetails`. +- Track only successful rules assignments. +- Keep online and precomputed behavior unchanged. +- Add rules-only and mixed-configuration tests. +- Use the fake engine for state-matrix tests. + +The main review question is: + +> Does `FlagsClient` select the correct path and return the correct result? + +## PR3 — Provider activation and customer experience + +Expose dynamic evaluation through the existing offline provider. + +- Pass the effective resolution context to `FlagsClient`. +- Pass the resolution logger to `FlagsClient`. +- Keep `initialize` network-free. +- Keep `onContextChange` network-free. +- Keep current provider event mapping. +- Add hook-context tests. +- Add real-provider integration tests. +- Update the provider documentation. +- Update both example applications. +- Add compatibility and bundle checks where the repository supports them. + +The main review question is: + +> Does the existing offline provider expose dynamic rules without changing online or precomputed behavior? + +## CI loop + +After PR3 is open, check PR1 first. +Fix PR1 until its checks pass. +Then, update PR2 with the PR1 fixes. +Fix PR2 until its checks pass. +Then, update PR3 with the PR2 fixes. +Fix PR3 until its checks pass. + +Repeat this order until all three pull requests are green: + +```text +PR1 -> PR2 -> PR3 -> PR1 +``` From 4920bcd1de6f148afe4b25cdf67e4d52de95875d Mon Sep 17 00:00:00 2001 From: Blake Date: Mon, 27 Jul 2026 15:47:48 -0400 Subject: [PATCH 7/8] docs: update dynamic offline plans for upstream progress --- dynamic_offline.plan.md | 772 ----------------------------- dynamic_offline_pr_stack.plan.md | 39 +- dynamic_offline_simplified.plan.md | 633 +++++++++++------------ 3 files changed, 318 insertions(+), 1126 deletions(-) delete mode 100644 dynamic_offline.plan.md diff --git a/dynamic_offline.plan.md b/dynamic_offline.plan.md deleted file mode 100644 index 2ed3ec520..000000000 --- a/dynamic_offline.plan.md +++ /dev/null @@ -1,772 +0,0 @@ -# FFL-2837 — Dynamic (rules-based) offline init in dd-sdk-reactnative - -**Jira:** [FFL-2837 — Building Blocks API for dynamic offline init in ReactNative SDK](https://datadoghq.atlassian.net/browse/FFL-2837) -**Base branch:** `blake.thomas/FFL-2666` (static/precomputed offline; OfflineProvider + `setConfiguration`) -**Work branch:** `blake.thomas/FFL-2837` (this branch, cut from FFL-2666) -**Upstream reference:** [openfeature-js-client PR #336 — FFL-2753 browser `CoreProvider`](https://github.com/DataDog/openfeature-js-client/pull/336) (`sameerank/FFL-2753-browser-core-provider`) - -### Source docs (drafts — both dated Jun 2026, status "In review", so names/versions may still move) -- [Portable Flag Configuration RFC](https://docs.google.com/document/d/1OWNBtXtSk535VXqf-9fqsAmU9W8kpFLAwxYi2y1qyQQ/edit?pli=1&tab=t.0#heading=h.n52036mkzewg) (local snapshot: `./Portable-Flag-Configuration-RFC.md`, repo root, untracked) — defines the building blocks: `ConfigurationWire`, `configurationFromString/ToString`, `CoreProvider`, fetch fns, hooks. -- [Offline Initialization for Feature Flagging RFC](https://docs.google.com/document/d/1q1GlEbAgCGuO1OWfGbmKQkk5Oo-rE7YQwq29kMJJ4II/edit?pli=1&tab=t.0#heading=h.rnd972k0hiyer) (local snapshot: `./Offline-Initialization-for-Feature-Flagging.md`, repo root, untracked) — offline recipes built from those blocks; the operation is always `configurationFromString(wire) → provider.setConfiguration(config) → evaluate(...)`. -- [ConfigurationWire (Confluence)](https://datadoghq.atlassian.net/wiki/spaces/PANA/pages/5141725646/ConfigurationWire) — the published wire spec. Its **protobuf/base64** rules encoding is the intended target (see §2.5), even though the current code still uses JSON. -- **RFC: Obfuscation for rules-based client configs** (local: `./RFC_Obfuscation_for_rules-based_client configs.md`, 2026-07-10, first draft, one approval) — defines the obfuscation design for client rules: per-flag opt-in switch, salted `ONE_OF_SHA256`/`NOT_ONE_OF_SHA256` operators (server-compatible, engine-evaluated), binary structure format, and "document what's exposed". Answers most of our obfuscation open question — see G6/D7. - ---- - -## 1. Objective - -Today (FFL-2666) the `DatadogOfflineOpenFeatureProvider` serves a **precomputed** configuration: -a single-subject snapshot carrying the exact context it was computed for. `setContext` on a -mismatched context puts the provider into `ERROR`. - -FFL-2837 adds the **rules-based** (dynamic) offline flow: - -- Customer loads a **rules-based** configuration (Universal Flag Configuration) via `setConfiguration` - — still **no network fetch** (this is the OfflineProvider). -- `setContext` / `onContextChange` **must not fetch**. It re-evaluates the loaded rules against the - new context locally and updates the served values. -- Rules-based config is **context-agnostic**: any context is valid; no "mismatch → ERROR". -- Evaluation logic is **imported from `@datadog/flagging-core`** (the already-published - `evaluateRulesBasedConfiguration()` rules engine — not the combined `evaluate()`; see D4/A), - not reimplemented in RN. - -Both flows coexist behind the same `DatadogOfflineOpenFeatureProvider`. A wire may carry -`precomputed`, rules, or both (precomputed preferred when its context matches, else rules). - -Per the Offline-Init RFC this is **not** a new SDK mode or a dedicated `offlineInit()` — it is the same -`configurationFromString → setConfiguration` loading path, differing only in the configuration kind. - ---- - -## 2. Current state of `@datadog/flagging-core` - -> ⚠️ **Corrected 2026-07-22 after code review** (verified against the installed -> `node_modules/@datadog/flagging-core@2.0.1` and PR #336, not a stale local branch). An earlier -> draft wrongly claimed 2.0.1 had "no evaluation module" — it does. See the corrected split below. - -### Published / consumed today (2.0.1) — what already ships -Verified in the installed package's `esm/` tree. **2.0.1 already contains the full rules engine and -root-exports it** (`esm/index.js`: `export * from './evaluation'` etc.): -- `evaluation/` — `evaluateForSubject`, `evaluateRulesBasedConfiguration`, `rules` (incl. `ONE_OF`, - `MATCHES` → `new RegExp(...)`), `matchesShard`, `sharders`, `ufc-v1` (`UniversalFlagConfigurationV1`, - `Flag`, `Allocation`, `Split`, `Shard`, `VariantType`, `variantTypeToFlagValueType`), `errors` - (`TargetingKeyMissingError`), `evaluationMetadata`. -- `obfuscation` — **generic MD5 helpers only** (`getMD5Hash`, `buildStorageKeySuffix`); no - obfuscated-rules decoder or context-transform pipeline (bears on G6/D7). -- `spark-md5` is a runtime dependency of 2.0.1. -- `configurationFromString` / `configurationToString` handling **only** the precomputed branch. -- `FlagsConfiguration = { precomputed?: PrecomputedConfiguration }` — **no `rulesBased` slot**. -- **Single `.` package export** — no subpaths (bears on the bundle discussion, D5). - -**Consequence for bundling:** RN's `packages/core/src/flags/configuration/wire.ts` already imports the -flagging-core **root barrel** (`configurationFromString`), and `core/src/index.tsx` re-exports it — so -under Metro (no tree-shaking) the entire engine + `spark-md5` **already ships in every -`@datadog/mobile-react-native` consumer today**, precomputed or not. (See D5 — this guts the old -"rules engine taxes every app" premise.) - -### What 2.0.1 is MISSING — added by PR #336 (unmerged, CONFLICTING/REVIEW_REQUIRED as of 2026-07-15) -Three things, all in `packages/core`: -- **`configuration/wire.ts`** adds the `rulesBased` wire branch — **the one RN actually needs.** -- **`configuration/configuration.ts`** adds `RulesBasedConfiguration` and `FlagsConfiguration.rulesBased?` - (the parsed-config slot RN reads; RN keeps the type internal — E). -- **`evaluation/evaluation.ts`** adds the combined **`evaluate(config, …)`** (precomputed-first → rules). - **RN does NOT use `evaluate()`** — D4 does path selection in RN and calls the rules-only - `evaluateRulesBasedConfiguration` (already in 2.0.1). So `evaluate()` is not an RN prerequisite (A). - -So the bump RN needs is for the **rules wire parsing + the `rulesBased` parsed slot** — not the engine -and not `evaluate()`. - -### Browser analog -- **`browser/src/openfeature/core-provider.ts`** — the browser `CoreProvider`, the closest analog to - the RN target. Behaviors to mirror (all confirmed by the RFC §CoreProvider): - - `getConfigurationError()` errors only when there is **no** evaluatable config, or a precomputed - config with mismatched context **and no rules fallback**. With rules present, any context is valid. - - `onContextChange` just stores the new context (no fetch); evaluation reads it live. - - `setConfiguration` emits `Ready` / `ConfigurationChanged` / `Error`. - -### Path-selection precedence (RFC §"Evaluation path selection") — RN implements this itself -The combined `evaluate()` encodes this order, but **RN does not call `evaluate()`** (D4/A) — it reproduces -the order in `FlagsClient` and calls the rules-only `evaluateRulesBasedConfiguration()` for step 2: -1. `precomputed` present **and** context matches → serve precomputed (from RN's decoded `Map`). -2. Else rules present → `evaluateRulesBasedConfiguration(rulesResponse, …)` (per-call, per-context). -3. Else `precomputed` present (mismatch, no rules) → `ERROR / INVALID_CONTEXT`. -4. Else → `ERROR / PROVIDER_NOT_READY`. -The match in step 1 must be evaluated **per resolution** (R12/B), not frozen at reconcile. - -Rules evaluation folds `targetingKey` into `subjectAttributes.id` **only when `subjectKey != null`** -and returns `ResolutionDetails` (value, variant, reason, errorCode, and `flagMetadata` with -`allocationKey`, `variationType`, `doLog`, `__dd_split_serial_id`, `__dd_allocation_key`, `__dd_do_log`, -and an eval timestamp). **`flagMetadata` does NOT include `extraLogging`** (PR #336 deliberately omits it) -— see G4/D3, this blocks faithfully rebuilding a `FlagCacheEntry` for native tracking. - -**Targeting-key semantics (verified in `evaluateForSubject.js`):** `TargetingKeyMissingError` is thrown -only when `subjectKey == null` (null/undefined) **and** a matching allocation has shards. An empty -string `''` is **not** null — it is bucketed as a real (anonymous) subject. See G8/D8: RN's -`EvaluationContext.targetingKey` is typed `string` (required) and its own docs tell callers to pass -`''`, so RN currently cannot express "missing" as `undefined`; and RN's `FlagErrorCode` -(`types.ts:168`) has no `TARGETING_KEY_MISSING` member. - ---- - -## 2.5 Wire format: three sources, and the intended encoding - -The rules wire shape is described **inconsistently** across the three references: - -| Source | `version` | rules field name | `response` encoding | -|--------|-----------|------------------|---------------------| -| ConfigurationWire (Confluence) — **intended target** | `1` | `rules` | **protobuf, base64** | -| flagging-core PR #336 `wire.ts` (**current code** — what RN calls today) | `1` | `rulesBased` | JSON string (`JSON.parse`) | -| `Portable-Flag-Configuration-RFC.md` | `2` | `server` | JSON-encoded `ServerConfiguration` | - -**Decided-but-not-yet-implemented: the rules `response` is intended to be protobuf-encoded, base64.** -The ConfigurationWire spec is the target. The current flagging-core code takes a shortcut and -`JSON.parse`s the rules `response` (as does the RFC prose) — that is the **not-yet-migrated** state, -not the destination. Treat protobuf as the plan of record; do **not** design RN around the JSON -shortcut persisting. - -> **Note:** the Obfuscation RFC's "binary format" (structure obfuscation) is *aligned with* but does -> **not by itself establish** protobuf. Protobuf is a separate ConfigurationWire decision, and its schema -> must be extended to carry the new **`ONE_OF_SHA256`/`NOT_ONE_OF_SHA256` operators and their salt -> fields** (G12). Don't conflate "binary" with "protobuf is decided". - -Implications for RN (all deferred until flagging-core lands the protobuf path): -- **Not an RN implementation blocker** — it is a **flagging-core release-contract** item. RN only - consumes the opaque parser (`configurationFromString`), so as long as the released API stays opaque, - RN neither knows nor cares whether the payload is JSON or protobuf. RN can build and test against - whatever the pinned version emits. -- **Decoding stays in flagging-core, not RN.** When flagging-core switches the rules branch from - `JSON.parse` to protobuf-decode, RN inherits it via the version bump with **no RN parsing code**. -- **Watch the transitive dep.** A protobuf runtime (e.g. `protobufjs` or generated decoders) would be - pulled in transitively — folds into the Hermes+JSC review (R5). It is **one of two** genuinely new bundle - masses (the other is the synchronous SHA-256 for obfuscation, G11); the evaluation engine already ships - (§2, D5). -- **The `.proto` schema is a prerequisite** and does not exist in a consumable form yet — track it as - part of G2 below. - -**Residual naming/version churn:** the field name (`rules` vs `rulesBased` vs `server`) and `version` -(1 vs 2) are still in flux across drafts. Pin to the released flagging-core version, re-verify the -exported type/name before wiring RN types (R8), and never hard-code the wire field name in RN. - ---- - -## 3. Gaps in `@datadog/flagging-core` we must bridge - -| # | Gap | Blocking? | Action | -|---|-----|-----------|--------| -| G1 | `rulesBased` wire parsing + parsed-config slot are unmerged (PR #336) and unpublished | **Yes** | The engine **and the rules evaluator (`evaluateRulesBasedConfiguration`) already ship in 2.0.1** (§2, A). Only the wire parsing + the `rulesBased` parsed-config slot are missing — **not `evaluate()`** (RN doesn't use it). Land PR #336's `packages/core` changes, publish. Bump the dep **in `packages/core` only** (not react-native-openfeature — G-dep note). Develop against a linked / `npm pack` build meanwhile. PR #336 is CONFLICTING/REVIEW_REQUIRED, so its shape may still shift. | -| G2 | Rules `response` protobuf encoding not yet implemented | **Upstream only (not an RN blocker)** | Intended encoding is **protobuf/base64** (Confluence); current code `JSON.parse`s it. This is a flagging-core release-contract item: it must (a) publish the `.proto` and (b) switch its rules branch to protobuf-decode. RN consumes the opaque parser and inherits the switch on bump; only the transitive protobuf runtime touches RN (Hermes review, R5). Dev/tests run against whatever the pinned version emits. | -| G3 | Root exports for the symbols RN imports | **Mostly satisfied** | 2.0.1 already root-exports `UniversalFlagConfigurationV1` **and `evaluateRulesBasedConfiguration`** — the two symbols RN actually needs. RN does **not** need `evaluate` or a public `RulesBasedConfiguration` type (kept internal — E). Only remaining check: `configurationFromString` populates the rules branch after the bump (R8). | -| G4 | **Exposure/telemetry parity — one missing field** | **Yes (blocking)** | Narrowed (H): the rules `flagMetadata` **already carries** `__dd_split_serial_id` (serialId) and `__dd_eval_timestamp_ms` (timestamp) — **only `extraLogging` is missing**. Conversely RN's `FlagCacheEntry`/native bridge have **no slot** for serialId/timestamp, so exposing those upstream would not help RN transmit them anyway. **Action:** define the *exact* native `trackEvaluation` payload the offline rules path must send, then have upstream expose **`extraLogging`** on the rules result (or provide a rules-tracking API). Also confirm whether native telemetry needs the **original UFC variation type** — the engine collapses `INTEGER`/`NUMERIC` → `'number'` (`ufc-v1.js:8`), losing the distinction the precomputed path preserves. | -| G5 | Bundle size / Hermes+JSC compat | **Low today, two future adds** | The engine + `spark-md5` **and the rules evaluator already ship** via RN's existing root-barrel import (§2), so enabling rules adds only the rules wire branch — negligible now. **Two future upstream additions are genuinely new mass:** the protobuf runtime (G2) and a **synchronous SHA-256** (G11). A precomputed-only split would help **only** if flagging-core adds subpath exports (2.0.1 has none) *and* RN reworks its `wire.ts` barrel import. Verify `sharders.ts`/`spark-md5` (and later protobuf + sync SHA) run under **Hermes and JSC**. Measure before doing anything. | -| G6 | Obfuscation operators absent → **silent fallback today** | **Yes (hard prereq)** | Obfuscation = salted `ONE_OF_SHA256`/`NOT_ONE_OF_SHA256` operators (from `ONE_OF`, server-compatible, engine-evaluated) — but they're **absent from 2.0.1**, and an unknown operator makes `isValidRule` fail → `evaluateForSubject` returns **`DEFAULT` with no error** (verified). So an obfuscated rule silently serves the coded default — could wrongly enable/disable a feature. Require: (a) SHA operators shipped upstream **before** declaring dynamic client rules supported; (b) **load-time rejection of unsupported operators as `GENERAL`**, not a normal `DEFAULT`; (c) a capability/version mechanism so the service won't send SHA operators to older SDKs; (d) tests for unknown-future operators and **cached configs used after an SDK downgrade**. | -| G11 | SHA-256 must be **synchronous** and Hermes/JSC-safe | **Yes (upstream)** | The whole eval path is synchronous (`FlagsClient.get*Details`, the OF resolvers), but Web Crypto `SubtleCrypto.digest()` is **async** — unusable here. Upstream needs a **synchronous** SHA-256 (pure-JS or WASM, no Node `crypto`, no browser-only Web Crypto, no unavailable globals), Hermes **and** JSC compatible across our supported RN range. **This is new bundle mass** — invalidates the "protobuf is the only new mass" claim (D5). Add SHA-specific release-build perf + bundle measurements. | -| G12 | The portable salted-hash **protocol is unspecified**, and malformed SHA conditions need load-time rejection | **Yes (upstream contract)** | The RFC names the operators + "random salt" but defines neither the condition schema nor the hash input encoding. Before implementation the shared contract must pin: salt length/encoding; ordering (`salt‖value` vs `value‖salt` vs framed); UTF-8 + Unicode normalization; digest encoding (hex/base64, casing, padding); how numbers/booleans stringify (`ONE_OF` uses JS `value.toString()` — **not** automatically portable); empty-string / null-or-missing attribute / `NOT_ONE_OF_SHA256` behavior; and **canonical cross-SDK test vectors**. The generator, JS evaluator, and every server/mobile evaluator must produce **identical bytes** — "RN strings pass through unmodified" is necessary but far from sufficient. **Once the schema exists, load-time validation must also reject** (before READY): missing/malformed salt; salt of wrong length/encoding or unreasonably large; digests of wrong length/encoding; non-string digest-array entries; malformed `ONE_OF_SHA256`/`NOT_ONE_OF_SHA256` condition shapes; and excessive condition/value counts that could block the JS thread. | -| G7 | Untrusted rules wire is not validated, and regex safety is not solvable in RN alone | **Yes (high)** | `evaluateRulesBasedConfiguration` derefs `config.flags[flagKey]` **before** its try/catch (throws on a malformed UFC), and `rules.ts` builds `new RegExp(...)` from wire patterns. Structural validation on load (envelope, flags map, allocations/splits/variations, shard ranges, inherited keys `__proto__`/`toString`) is doable in RN. **Catastrophic-regex (ReDoS) is NOT reliably detectable by "deep validation" alone** (F) and executing a hostile pattern in a test can hang Jest/Hermes. Require one concrete mitigation — **preferably upstream/shared**: an upstream safe-regex validation guarantee (trusted config), a static safe-regex policy/library, or a bounded regex engine. Run adversarial perf tests in an **isolated, time-bounded** process. **Clone the snapshot before freezing** (do not freeze the caller's object). | -| G8 | Missing vs anonymous targeting key is not representable in RN | **Yes** | Rules bucket `''` as a real subject and only raise `TARGETING_KEY_MISSING` for `null`/`undefined`, but RN's `EvaluationContext.targetingKey` is required `string` (docs say pass `''`) and `FlagErrorCode` lacks `TARGETING_KEY_MISSING`. Decide whether missing and anonymous-empty are distinct; if so, preserve `undefined` through the rules path and add `TARGETING_KEY_MISSING` to `FlagErrorCode`. | -| G9 | Prototype-unsafe flag lookup returns `DISABLED` for absent reserved-name flags | **Yes (bug)** | `config.flags[flagKey]` on the plain UFC object resolves `toString`/`__proto__`/`constructor` through `Object.prototype`, so an **absent** flag with such a key yields `DISABLED` instead of `FLAG_NOT_FOUND`. RN own-property-guards before delegating (`Object.hasOwn`), **or** upstream switches to an own-property check / null-prototype dict. (Note the *precomputed* path already dodges this by using a `Map`.) | -| G10 | OpenFeature type/dependency boundary is unsound | **Yes** | `packages/core` declares **no** `@openfeature/*` dependency, yet flagging-core's published `.d.ts` import `@openfeature/core` while listing it only as a **devDependency** (`flagging-core/package.json:43`). Types resolve only via hoisting. RN must not add a bare `@openfeature/*` import to core — pass structurally-compatible internal types from react-native-openfeature, or add an explicit core dep **and** have flagging-core fix its published dep (§Step 5, D11, R13). | - ---- - -## 4. Implementation steps in dd-sdk-reactnative - -Architecture we inherit: -- `DatadogOfflineOpenFeatureProvider` (react-native-openfeature) — OpenFeature lifecycle → maps to - `FlagsClient` reconcile results + provider events. -- `FlagsClient` (core) — owns `loadedConfiguration`, `reconcile()`, served cache, evaluation. -- `configuration/` (core) — wire parse + decode + context helpers. - -The precomputed path **precomputes a `Map`** at load and serves from it. -The rules path is **lazy**: it cannot precompute all flags, so it evaluates per `getX` call against -the live context via flagging-core's **`evaluateRulesBasedConfiguration()`** (see the design note below). - -> **Design note — call `evaluateRulesBasedConfiguration()`, not `evaluate()` (A).** D4 does the -> precomputed-vs-rules selection **in RN**, so the combined `evaluate()` (whose whole job is that -> arbitration) is unnecessary and would risk routing precomputed data through upstream arbitration. -> `evaluateRulesBasedConfiguration(ufc, type, key, default, ofContext, logger)` is **already public in -> 2.0.1** (`node_modules/@datadog/flagging-core/esm/evaluation/evaluation.d.ts:4`) and takes the UFC -> (`config.rulesBased.response`) directly — no fake `FlagsConfiguration` to construct. This also removes -> `evaluate()` from RN's prerequisites: the bump is needed **only** for the `rulesBased` wire-parsing + -> the parsed config slot, not for the evaluator. - -### Step 0 — Prereqs (§3) -- [ ] PR #336 core changes merged; `@datadog/flagging-core` published with the **`rulesBased` wire - parsing + parsed-config slot**. (The evaluator — `evaluateRulesBasedConfiguration` — and - `UniversalFlagConfigurationV1` already ship in 2.0.1, so `evaluate()` is **not** a prerequisite.) -- [ ] Bump `@datadog/flagging-core` **in `packages/core/package.json` only** (`:119`); update `yarn.lock`. - `packages/react-native-openfeature` has **no** direct flagging-core dependency — it consumes it - transitively through its `@datadog/mobile-react-native` peer dep (`react-native-openfeature/package.json:40,49`). -- [ ] Re-verify the published rules field name / version and that `configurationFromString` populates the - rules branch (R8). `evaluateRulesBasedConfiguration` + `UniversalFlagConfigurationV1` are already exported. -- [ ] Resolve the G4 blocker with upstream (expose `extraLogging` for rules eval) **before** building the tracking path. -- [ ] **Pin the actual post-SHA flagging-core version** for the obfuscation milestone — the one that ships - the SHA operators (G6) **and** the bundled **synchronous SHA-256** (G11) **and** the specified hash - protocol (G12). This is a *separate, later* bump from the wire-parsing bump above; obfuscated offline - rules are not supportable until it lands. Confirm no `evaluate()`/protobuf assumptions leak in. - -### Step 1 — Parsed-config type surface (`core/src/flags/configuration/types.ts`) -- [ ] Don't export a **named** rules/UFC type: use `UniversalFlagConfigurationV1` (already in 2.0.1) as the - internal rules-response type and keep any `ParsedRulesBasedConfiguration` alias internal (not re-exported). -- [ ] **But note the opacity claim is only partial (item 2 / R16 / D10).** `ParsedFlagsConfiguration` - (= upstream `FlagsConfiguration`) is **already public** (`types.ts:47` → `index.tsx:128`) and is the - type customers receive from `configurationFromString` and pass to `setConfiguration`. Once upstream adds - `FlagsConfiguration.rulesBased`, that public alias **structurally exposes** `rulesBased.response` (the - UFC) — exactly as it already exposes `precomputed.response` today — so *not* exporting the sub-type does - **not** make the schema opaque. Resolve **D10**: either (a) **accept structural visibility** and soften - the "opaque" language (pragmatic — precomputed is already visible), or (b) redesign - `ParsedFlagsConfiguration` as a **branded/opaque** type (a breaking type-compat change) so customers can - pass it through but not inspect/construct it. Do not claim opacity while shipping (a). - -### Step 2 — Wire parsing (`core/src/flags/configuration/wire.ts`) -- [ ] **No RN code change expected**: RN re-exports `configurationFromString`/`configurationToString` - from flagging-core; the bumped version handles the rules branch automatically — including the future - switch from the interim JSON shape to **protobuf/base64** decode (§2.5), which RN inherits with no - parsing code of its own. -- [ ] Add an RN test asserting a rules wire round-trips through the re-export (guards the bump + the - field-name churn in R8). Use whatever encoding the pinned flagging-core version emits (JSON interim, - protobuf once landed). - -### Step 3 — ONE precise state model + `loadConfiguration` (`core/src/flags/FlagsClient.ts`) -`loadConfiguration` already carries a **"FORWARD-COMPAT SEAM"** comment (FlagsClient.ts:247-255) saying -rules must be handled **before** the precomputed guard. **Define one unambiguous state model** — the -earlier draft was self-contradictory (a `kind: 'rulesBased'` state vs. "let `evaluate()` arbitrate over -the full config"; these disagree on which path serves a matching precomputed config). -- [ ] Model: **retain the whole parsed `FlagsConfiguration`** plus an **optional validated precomputed - `Map`** (decoded once via `decodePrecomputedFlags`) plus a **validated rules snapshot** (`UniversalFlagConfigurationV1`, G7). - Selection order mirrors the combined `evaluate()`: matching precomputed → rules → error — but RN - implements it itself and calls only the **rules-only** `evaluateRulesBasedConfiguration()` (A). Serve - precomputed from the `Map`; enter rules only when precomputed is absent/mismatched. **Decide the match - per resolution, not once at reconcile** (B) — see Step 5. `reconcile()` still sets overall - readiness/`configurationStatus`, but must not freeze the precomputed-vs-rules choice against a context - a later hook-mutated resolution won't share. -- [ ] **Mixed-validity — split by failure stage (D).** `configurationFromString` in PR #336 parses - **both** branches inside **one** `try/catch` (`JSON.parse(precomputed.response)` **and** - `JSON.parse(rulesBased.response)`), and the `catch` returns `{}`. So: - - **Parse-time corruption** (either `response` is not valid JSON/protobuf) → the **whole wire collapses - to `{}`**; RN never receives the valid sibling. This is **atomic** unless flagging-core switches to - per-branch parsing — RN cannot isolate it. Classify the empty result as `GENERAL` (as today). - - **Structurally-invalid-but-decoded branch** (JSON parsed, but the UFC/precomputed shape is bad) → RN - **can** isolate this during its own per-branch validation (Step 3 model / G7): keep the valid branch - servable, mark the bad branch unusable, and never silently fall back to it. - Enumerate both stages in tests; do not promise recovery of a parse-corrupted sibling. -- [ ] Validate the rules snapshot on load (G7) — do not store an unvalidated UFC that a later - evaluation will throw on. Rules are handled **before** the `!precomputed` guard so they are never - misclassified as `GENERAL`. -- [ ] **Handle unsupported operators — but push validation to the right layer (G6 — critical).** The - engine treats an unknown operator (e.g. `ONE_OF_SHA256` on an SDK without it) as an invalid rule and - **silently falls back to `DEFAULT` with no error** (only literally *fail-open* if that coded default is - permissive; otherwise it's a silent wrong value). This must not reach a silent default — but **RN should - not hand-maintain a "known operators" set**: it would duplicate flagging-core's schema and drift (reject - an operator the upgraded evaluator supports; accept one whose impl is absent/incompatible; and the future - protobuf decoder may map unknown enums to a number / `UNSPECIFIED` / drop them before RN ever sees a - string). Preferred fixes, in order: - 1. **Upstream** exposes `validateRulesConfiguration()` / a capabilities API, **or** the evaluator/parser - surfaces an unsupported operator as `GENERAL` instead of a silent `DEFAULT`. - 2. If RN needs a temporary guard, **derive it from the pinned `OperatorType` enum** (exported by - flagging-core, `rules.d.ts:3`) — never a separately maintained list — and reject unknowns as `GENERAL`. -- [ ] **Capability negotiation needs a concrete owner:** - - **Official fetch path:** the request advertises the evaluator's capabilities; the service omits or - rejects flags the SDK can't evaluate. - - **Portable/offline wire (this provider):** embed required capabilities / a minimum evaluator version in - the wire, **or** guarantee the parser preserves-and-rejects unknown operators. (Owner TBD with Core.) -- [ ] **Unsupported operators must fit the branch-level validity model (reconciles the tension with the - mixed-validity rule above).** An unsupported operator invalidates the **rules branch**, not necessarily - the whole config. Define the matrix: - - **Rules-only + unsupported operator** → `GENERAL`. - - **Valid precomputed + unsupported rules:** the rules branch is unusable but the precomputed branch is - **retained and served when its context matches** (consistent with the mixed-validity decision) — do - **not** reject everything. - - **Same, context mismatched** (so precomputed can't serve and rules are unusable) → `GENERAL` (the - config is genuinely unusable here), **not** `INVALID_CONTEXT`. - - **Per-resolution (B):** provider stays `READY` while precomputed matches; a resolution whose (possibly - hook-mutated) context falls through to the invalid rules branch returns `GENERAL`. - - **Granularity:** invalidate the **whole rules branch** (simplest, safest) rather than per-flag — a - single unsupported operator means RN can't trust that branch's evaluation. (Revisit only if per-flag - isolation is later justified.) -- [ ] **Validate SHA condition shape at load once the protocol exists (G12).** Beyond "is this operator - known", a `ONE_OF_SHA256`/`NOT_ONE_OF_SHA256` condition carries a salt + digest array that is untrusted - wire data. Reject before READY: missing/malformed/oversized salt, wrong salt or digest length/encoding, - non-string digest entries, malformed condition shapes, and excessive condition/value counts (thread-block - guard). Same predictable-failure discipline as the precomputed decoder. - -### Step 4 — `reconcile()` for rules (`core/src/flags/FlagsClient.ts`) -- [ ] When the active path is rules: **any** external context is valid (no `INVALID_CONTEXT`). - `configurationStatus = 'ready'`. Do **not** populate `flagsCache` (lazy). -- [ ] **Targeting-key decision (G8) — do NOT blindly manufacture `{ targetingKey: '' }`.** `''` buckets - as a real anonymous subject, so every keyless context would bucket **identically** and a "missing key" - could never surface `TARGETING_KEY_MISSING`. Decide the semantics: - - If missing ≡ anonymous-empty is acceptable, keep `''` and document that keyless == one shared bucket. - - If they must differ, thread `undefined` (not `''`) into the rules path when no key is set — which - requires relaxing the internal context typing and adding `TARGETING_KEY_MISSING` to `FlagErrorCode`. -- [ ] Leave the precomputed branch unchanged. The precomputed mismatch→`INVALID_CONTEXT` fires **only** - when there is no rules fallback (mirror browser `getConfigurationError`). - -### Step 5 — Evaluation path (`core/src/flags/FlagsClient.ts` `getDetails`) -Largest change. `getDetails` currently serves from `flagsCache` (precomputed/online). -- [ ] For rules, call **`evaluateRulesBasedConfiguration()`** (rules-only; A) with the UFC directly: - ```ts - import { evaluateRulesBasedConfiguration } from '@datadog/flagging-core'; - const details = evaluateRulesBasedConfiguration(rulesResponse, type, key, defaultValue, ofContext, logger); - ``` - `rulesResponse` is `configuration.rulesBased.response` (a `UniversalFlagConfigurationV1`). It takes an - **OpenFeature-flat** context (`{ targetingKey, ...attrs }`) while `FlagsClient` holds - `{ targetingKey, attributes }` — add an adapter (inverse of `toDdContext`/`normalizeWireContext`); the - engine folds `targetingKey → subjectAttributes.id` only when non-null (G8). -- [ ] **Select the path against the RESOLUTION context, not the stored one (B).** `resolveX` currently - discards its `context` arg (`coreProvider.ts:61`) and `FlagsClient` uses the stored context. But a - **`before` hook can mutate the evaluation context for a single resolution** (see OpenFeature context - spec), so the context actually being evaluated can differ from the one `reconcile()` last saw. If the - precomputed context-match check was decided at reconcile-time against the stored context, a hook-mutated - resolution could be served a **precomputed value for a context it does not match** — an assignment leak. - So the **precomputed-vs-rules selection (esp. the precomputed context match) must run per-resolution - against the effective resolution context**, or the provider must explicitly ignore/forbid `before`-hook - context changes. Per-resolution selection still keeps the O(1) precomputed `Map` lookup. -- [ ] **Thread the resolution context + logger — but resolve the dependency boundary first (item 5 + N).** - The web SDK is **static-context**: clients/invocations must **not** supply context (OpenFeature forbids - it), so the fix is *not* "don't drop invocation context" — there is none. Context comes from - global/domain state **plus `before`-hook mutations**, and the resolver receives that effective context. - Thread the resolver-provided context + logger into the rules evaluation (as the browser `CoreProvider` - does) and test against the client-side model (global/domain + hooks). **Boundary problem (G10/R13):** - `packages/core` has **no** `@openfeature/*` dependency (`package.json` — zero refs), and flagging-core - publishes `.d.ts` that `import @openfeature/core` while listing it only as a **devDependency** - (`flagging-core/package.json:43`) — so the `Logger`/`ResolutionDetails`/`EvaluationContext` types work - today only by **hoisting accident**. Do **not** add a bare `@openfeature/*` import to core. Pick one: - (a) keep OpenFeature types in `react-native-openfeature` and pass **structurally-compatible internal - context/logger types** into `FlagsClient` (no core dep); or (b) add an explicit `@openfeature/*` - dependency to `packages/core` **and** require flagging-core to declare `@openfeature/core` as a real - dependency. Also decide whether threading widens the **public** `FlagsClient.get*Details` API or goes - through a **separate internal entry point** — prefer the latter to avoid a public API change. -- [ ] **`id` vs `targetingKey` precedence — decide, don't just "watch" (D9/item 4).** The engine builds - `subjectAttributes = { id: subjectKey, ...remainingContext }` (`evaluation.js:15`), so a customer - **`id` attribute overrides** the synthetic targeting-key `id` used for **rule matching**, while - **sharding still uses `subjectKey`** (`selectSplitUsingSharding`) — targeting and bucketing would then - key off different identifiers. Choose and enforce a contract: **reserve `id` for the targeting key** - (drop/reject a customer `id` in the flat adapter — recommended), or deliberately document the upstream - override. Assert the chosen result in tests; do not leave it to "watch". -- [ ] Map `ResolutionDetails` → RN `FlagDetails` (value, variant, `allocationKey`/`doLog` from - `flagMetadata`, reason, errorCode/errorMessage). **`evaluateRulesBasedConfiguration` already returns - `FLAG_NOT_FOUND`** for an absent flag (`evaluation.js:21`), plus `TYPE_MISMATCH`/`DISABLED`/`DEFAULT`/ - `TARGETING_KEY_MISSING`/`GENERAL` — **map them through; RN does not synthesize `FLAG_NOT_FOUND`** - (round-2 said otherwise — corrected). -- [ ] **Prototype-safe flag lookup (G9 — bug).** The evaluator does `config.flags[flagKey]` on a plain - object, so a **missing** flag named `toString`/`__proto__`/`constructor` resolves through - `Object.prototype` (truthy) and returns **`DISABLED`, not `FLAG_NOT_FOUND`**. Before delegating, guard - with `Object.prototype.hasOwnProperty.call(rulesResponse.flags, key)` (or `Object.hasOwn`) and return - `FLAG_NOT_FOUND` yourself when it is not an own property — **or** require an upstream own-property/ - null-prototype-dict fix. Test **absent reserved-name flags** (not just malformed configs that contain - those names). -- [ ] **Exposure/telemetry tracking (CORRECTED — D3):** the native side (Android `trackResolution`, iOS - `trackEvaluation`) expects **every successful assignment** to cross the bridge and applies `doLog` - itself — `doLog` gates **only the exposure event**, while RUM (gated by `rumIntegrationEnabled`) and - evaluation telemetry (gated by `trackEvaluations`) fire independently. So call - `this.track(entry, ctx)` for **every successful assignment, NOT gated on `doLog`** — matching the - existing precomputed path (`FlagsClient.ts:441`, which is therefore correct, not a bug). Track **only - when a variant was actually assigned** — do **not** track for `DISABLED`, unmatched/no-variant - `DEFAULT`, `TYPE_MISMATCH`, `FLAG_NOT_FOUND`, or error results (matches precomputed's early returns). - **Blocked by G4:** the synthesized `FlagCacheEntry` needs `extraLogging`, which the rules - `flagMetadata` does not carry. Resolve G4 before implementing this bullet. -- [ ] Keep precomputed + online paths serving from `flagsCache` untouched. - -### Step 6 — Path selection when both present + bundle-size decision -- [ ] **Two paths (DECIDED — D4), one precise per-resolution selection (see Step 3 + Step 5/B).** Serve - matching precomputed from the decoded `Map` (O(1)); take the rules branch - (`evaluateRulesBasedConfiguration(rulesResponse, …)`) **only** when precomputed is absent or its context - does not match **the effective resolution context**. Because RN calls the **rules-only** evaluator with - just the UFC, precomputed data is excluded from the upstream call **by construction** — there is no - full-config `evaluate()` arbitration to accidentally route precomputed through, and RN's - `decodePrecomputedFlags` validation is never bypassed. Do the precomputed context-match per resolution - (B), not once at reconcile. Factor the shared `ResolutionDetails → FlagDetails` + - synthesize-`FlagCacheEntry` mapping into one helper. -- [ ] **Bundle size (CORRECTED — D5): the engine already ships; measure, then likely do nothing.** The - earlier premise was wrong: RN's `wire.ts` already imports the flagging-core **root barrel**, so under - Metro the whole engine + `spark-md5` is **already in every `@datadog/mobile-react-native` bundle** - today (§2, G5). The rules evaluator (`evaluateRulesBasedConfiguration`) also already ships, so enabling - rules adds only the rules wire branch — negligible. Actions: - 1. **Measure separate baselines** (item 6): current baseline, root-SDK import, online flags, - precomputed offline, dynamic offline. Attribute the delta correctly. - 2. Dynamic `import()` is **rejected** — Metro does not code-split release bundles, so it wouldn't - shrink anything (Metro module API confirms). - 3. A precomputed-only split (`DatadogPrecomputedOfflineProvider` + a flagging-core - `@datadog/flagging-core/precomputed` subpath) is the *only* real size lever — but it helps **only - if** flagging-core adds subpath exports (2.0.1 has just `.`) **and** RN moves its `wire.ts` import - off the root barrel onto that subpath. Pursue only if the measured delta actually justifies it. - The genuinely new mass to watch is **two future upstream additions**: the protobuf runtime (G2) **and - a synchronous SHA-256 implementation** for obfuscation operators (G11) — *not* the existing engine. (This - split is a bundle lever only — not a security gate; see D6.) - -### Step 7 — `DatadogOfflineOpenFeatureProvider` (`react-native-openfeature/src/offlineProvider.ts`) -- [ ] `initialize` / `onContextChange` already do not fetch. For rules, `applyContext`'s reconcile - returns `ready` for any context, so the mismatch-throw path disappears naturally. Verify the - empty-context handling for rules — **but this depends on the unresolved D8** (missing vs anonymous): - there is no embedded context to re-adopt, so an empty context is *either* an anonymous subject (`''`) - *or* a "no key" signal, per whatever D8 decides. Do not hard-code the anonymous interpretation here - until D8 is settled. -- [ ] **Update the class doc comment** — it currently states precomputed-only semantics ("you should - **not** call `OpenFeature.setContext`"). For rules, `setContext` **is** the intended dynamic path. -- [ ] `setConfiguration` event mapping (`Ready` / `ConfigurationChanged` / `Error`) is already generic - and matches the RFC event model; confirm a rules `ready` triggers the right transitions. -- [ ] **Opt-in posture (D6 — justification splits by config source).** For **Datadog-generated** configs - the platform per-flag switch is the opt-in the Offline-Init RFC asks for - (`Offline-Initialization-for-Feature-Flagging.md:95`, `:158`), enforced server-side — but the RFC is a - first draft (per-flag vs per-org unsettled) and **scoped client tokens don't exist yet** (RFC:109). For - **customer-supplied/bundled** wires (which this provider accepts) platform controls are **bypassed**, so - `setConfiguration(rules)` is the opt-in and the customer owns supplying client-appropriate rules. Either - way the **RN offline provider needs no additional gate**. Keep product/security sign-off on record; ship - the docs caveat + "what stays visible" list regardless. - -### Step 8 — Exports, examples & docs -- [ ] **Do NOT export a named rules/UFC type — but that alone does not make the config opaque (E + D10).** - Keep `ParsedRulesBasedConfiguration`/`UniversalFlagConfigurationV1` unexported. **However**, the already-public - `ParsedFlagsConfiguration` structurally carries `rulesBased` after the bump (item 2 / R16), so decide - **D10** here: soften the opacity claim (accept it, as with precomputed today) or brand - `ParsedFlagsConfiguration`. Don't ship docs/marketing that call the config opaque under option (a). -- [ ] **Full two-flow README rewrite (not just a new snippet).** `react-native-openfeature/README.md` - (≈`:146`–`:169`) currently documents **precomputed-only** semantics: single-subject snapshot, the - "do **not** call `OpenFeature.setContext` with a different context" warning, the empty-context/`''` - re-adopt caveat, and the dedicated-domain/`clientName` guidance. The rules flow **inverts** much of - this (`setContext` *is* the dynamic path; there is no embedded context to re-adopt). Rewrite the - offline section to present both flows side-by-side and clearly scope each caveat to precomputed. -- [ ] Update example apps (`example/src/flags/flagsProvider.ts`, - `example-new-architecture/flags/flagsProvider.ts`) with a rules-based offline flow - (`configurationFromString(wire) → setConfiguration → setContext(a) / setContext(b)`). -- [ ] Update the class doc comment on `DatadogOfflineOpenFeatureProvider` for the two-flow behavior. -- [ ] **Document precisely what stays visible in a rules config (D7 threat model) — cover ALL shipped UFC - data, not just names.** Don't call it "confidential". Per `ufc-v1.d.ts`, the config ships: flag/variant/ - attribute **names**; variant **values**; regex/numeric/version **operands**; the decodable config - **structure**; **guessable hashed membership values** (salted `ONE_OF_SHA256` resists precomputation but - not offline enumeration of low-entropy values); **and** allocation **keys**, split **serialIds**, - **`extraLogging`**, **`doLog`**, allocation `startAt`/`endAt`, shard **salts**, the **environment name**, - and **`createdAt`**. Flag `extraLogging` especially — the plan is explicitly blocking on exposing it for - tracking (G4). Consider the RFC's UI lock-icon idea. - ---- - -## 5. All imports added to dd-sdk-reactnative - -From `@datadog/flagging-core`: -- `evaluateRulesBasedConfiguration` — **static** value import in `core/src/flags/FlagsClient.ts` - (already public in 2.0.1; A). No dynamic import (D5: it doesn't reduce Metro bundle size). **Not - `evaluate`.** -- `type UniversalFlagConfigurationV1` (already exported by 2.0.1) — the internal rules-response type in - `configuration/types.ts` / `FlagsClient.ts`. `type FlagsConfiguration` / `FlagTypeToValue` as today. -- (already imported) `configurationFromString`, `configurationToString`, precomputed types. -- **`Logger`/`ResolutionDetails` — NOT imported bare into core (D11).** `packages/core` has no - `@openfeature/*` dep and flagging-core mis-declares `@openfeature/core` as a devDep (G10). Per D11: - either pass **structurally-compatible internal** context/logger types from react-native-openfeature into - an **internal** `FlagsClient` entry point, or add an explicit core `@openfeature/*` dep + fix flagging-core's - published dep. Do not widen `FlagsClient.get*Details` for this. -- **Kept internal, not re-exported:** any `RulesBasedConfiguration`/`ParsedRulesBasedConfiguration` alias (E). - -Internal (RN) new: -- A context adapter (RN `{targetingKey, attributes}` ↔ OpenFeature-flat `{targetingKey, ...attrs}`), - likely in `flags/configuration/context.ts` or `flags/internal.ts`. -- An internal `ParsedRulesBasedConfiguration` alias used only within `flags/` (no public export chain — E). - -**No new native surface required** — `NativeDdFlags.trackEvaluation(clientName, key, rawFlag, -targetingKey, attributes)` already accepts a synthesized flag object, and rules evaluation is entirely JS. -**Caveat (G4):** `rawFlag` must carry `extraLogging`, which the rules result does not expose today — -resolve upstream first, and confirm whether the collapsed `INTEGER`/`NUMERIC` → `number` variation type -is acceptable for native telemetry. - ---- - -## 6. Test plan - -Model on existing suites: `react-native-openfeature/src/__tests__/offlineProvider.test.ts`, -`offlineProvider.integration.test.ts`, `core/src/flags/__tests__/FlagsClient.test.ts`, -`core/src/flags/configuration/__tests__/wire.test.ts`. - -### Wire / config (core) -- [ ] `configurationFromString` parses a rules wire into `{ rulesBased: { response: UFC } }`. -- [ ] Round-trip `configurationToString(configurationFromString(wire))`. -- [ ] Malformed / unsupported-version wire → empty config (lenient) → classified `GENERAL` on load. -- [ ] Wire carrying **both** precomputed and rules parses both. -- [ ] Guard test that fails loudly if the published rules field name/version differs from expectations (R8). - -### FlagsClient (core) — rules load & reconcile -- [ ] `setConfiguration` rules-only → `ready` for an **empty** context. -- [ ] `setEvaluationContextWithoutFetching(anyContext)` on rules → `ready` (never `INVALID_CONTEXT`); **no** native fetch. -- [ ] `resetEvaluationContextWithoutFetching()` on rules → `ready`. -- [ ] No config loaded → `PROVIDER_NOT_READY` (regression guard). -- [ ] Both precomputed + rules: matching context serves precomputed; mismatch falls back to rules (not - `ERROR`); precomputed-only mismatch still `INVALID_CONTEXT`. - -### FlagsClient (core) — rules evaluation -- [ ] `getBooleanValue/String/Number/Object` return the rules-evaluated value for a targeting key that - matches an allocation; return the else/fallthrough variant otherwise. -- [ ] **Dynamic behavior:** changing context between two evaluations yields **different** values when - the two subjects bucket differently (the core point of FFL-2837). -- [ ] Flag-not-found → `FLAG_NOT_FOUND` + default (the evaluator returns this directly — map it through). - Type mismatch → `TYPE_MISMATCH` + default. -- [ ] **Prototype-named absent flag (item 1 bug):** evaluating a flag key `toString` / `__proto__` / - `constructor` that is **absent** from `flags` returns **`FLAG_NOT_FOUND`** (proves the own-property - guard works) — without the guard the evaluator returns `DISABLED`. Distinct from a malformed config - that merely *contains* such a key. -- [ ] **`id`/`targetingKey` precedence (D9):** a customer `id` attribute is handled per the chosen - contract (recommended: dropped so the targeting key is the sole subject id) — assert rule matching and - sharding key off the **same** identifier. -- [ ] **Disabled flag** (`enabled: false`) → default with `DISABLED` reason. -- [ ] **No matching allocation** / after `endAt` / before `startAt` → default allocation (`DEFAULT`). -- [ ] **Missing variant** (split references a variationKey absent from `variations`) → default. -- [ ] **Targeting-key semantics (G8):** `undefined`/null key with a sharded matching allocation → - `TARGETING_KEY_MISSING` **only if** RN decides missing≠anonymous and threads `undefined`; an **empty - string `''` buckets as a real subject** (assert two distinct empty-key contexts bucket the same). Do - not assert `TARGETING_KEY_MISSING` for `''` — it cannot occur. -- [ ] **Per-resolution path selection (B):** with **both** precomputed (matching stored context) and - rules loaded, a `before` hook that mutates the resolution context so it **no longer matches** the - precomputed snapshot must **not** be served the precomputed value — it must fall through to rules (or - error), i.e. the precomputed match is re-checked against the resolution context, not the stored one. -- [ ] Exposure/telemetry (D3): `native.trackEvaluation` called for **every successful assignment - regardless of `doLog`** (verify `doLog:false` still crosses the bridge so native RUM/eval telemetry - fire); assert the synthesized flag carries `variationKey`, `allocationKey`, `variationValue` string, - and `extraLogging` (blocked on G4). -- [ ] **Negative tracking (J):** `native.trackEvaluation` is **NOT** called for `DISABLED`, - unmatched/no-variant `DEFAULT`, `FLAG_NOT_FOUND`, `TYPE_MISMATCH`, or error results — only a real - assigned variant tracks (matches precomputed's early returns). -- [ ] **Validation / hostile input (G7):** malformed envelope (no `flags`), malformed flag, bad - shard/range, inherited-key flag (`toString`/`__proto__`), and **post-load mutation** of the passed - config object — each fails predictably (default + error, not a throw/hang from a READY provider). -- [ ] **Catastrophic regex (G7/F) — run in an isolated, time-bounded process** (a hostile pattern can - hang Jest/Hermes). Assert the chosen mitigation (upstream safe-regex guarantee / static policy / - bounded engine) holds; do not rely on structural validation to detect ReDoS. -- [ ] **Mixed configs (D):** (a) **parse-time** corruption of either branch collapses the whole wire to - `{}` (atomic — assert the valid sibling is *not* recoverable today); (b) **structurally-invalid but - decoded** branch is isolated — the valid sibling stays servable, the bad branch is never a silent fallback. -- [ ] Context threading uses the client-side model (global/domain + `before` hooks; no client/invocation - context — C), and the OpenFeature types reach `FlagsClient` via the chosen dependency boundary (D11), - not a bare `@openfeature/*` import in core. -- [ ] **Unsupported operator fails closed (G6):** a rule with an operator RN doesn't recognize - (`ONE_OF_SHA256` today, or any future operator) is **rejected at load as `GENERAL`** — assert it does - **not** silently evaluate to `DEFAULT`. Include a **cached-config-after-downgrade** case (a config with - newer operators loaded by an older SDK build). -- [ ] Obfuscation (D7): (a) attribute string values survive `processEvaluationContext` / the flat adapter - **unmodified** so the engine can hash them; (b) once upstream ships `ONE_OF_SHA256`/`NOT_ONE_OF_SHA256` - with a specified protocol (G12), evaluate the **canonical cross-SDK test vectors** (salt, stringification - of numbers/booleans, empty/null, `NOT_ONE_OF_SHA256`) — byte-identical to the generator/server, not just - "strings pass through". Until then those operators are an upstream gap (absent from 2.0.1). -- [ ] **Evaluation hot-path performance:** rules `getX` (incl. the sync SHA-256 once present) is fast - enough for repeated per-render calls; measure on a release build under Hermes and JSC. - -### Provider (react-native-openfeature) -- [ ] Rules config loaded before registration → provider reaches `READY` (no context needed). -- [ ] `setContext(ctxA)` then `setContext(ctxB)` re-evaluates locally, **no fetch**, values reflect each - context. Do **not** assert a `RECONCILING→READY` sequence: `onContextChange` is **synchronous** - (`offlineProvider.ts:103`) and intentionally skips the transient `RECONCILING` state; assert the - final `READY`/updated values and that no fetch occurred instead. -- [ ] `setConfiguration` valid rules → `CONFIGURATION_CHANGED` (and `READY` if recovering from error). -- [ ] `setConfiguration` empty/invalid → `PROVIDER_ERROR` with a top-level errorCode. -- [ ] Determinism: same (context, config) → same bucketed variant across calls. -- [ ] Regression: all existing precomputed offline tests still pass. - -### Integration / non-functional -- [ ] End-to-end: parse a real rules `ConfigurationWire` sample → set provider → evaluate several flags - across several contexts; assert values + exposure calls. Use a UFC fixture from ffe-service or the - flagging-core test fixtures. -- [ ] **Bundle-size check** (D5/G5): measure **separate baselines** — current baseline, root-SDK import, - online flags, precomputed offline, dynamic offline — and attribute the delta correctly (the engine - already ships, so the *incremental* rules cost is near-zero; the **real future adds are the protobuf - runtime (G2) and the synchronous SHA-256 (G11)** — measure each against the pinned post-SHA version). -- [ ] **Hermes AND JSC smoke test**: rules evaluation (incl. `sharders`/`spark-md5`, the future protobuf - runtime, and the future synchronous SHA-256) runs under **both engines** across the supported RN range; - confirm no Node `crypto` / browser-only Web Crypto / unavailable globals. -- [ ] **Integration prerequisites checklist** must include: the synchronous SHA dependency (G11), Hermes - **and** JSC coverage, and the **actual post-SHA flagging-core version** (Step 0) — not "protobuf only". - ---- - -## 7. Risks & unknowns - -1. **Rules wire parsing + parsed-config slot are unpublished (G1, G3).** The engine **and the rules - evaluator (`evaluateRulesBasedConfiguration`) already ship** in 2.0.1; only the wire parsing + slot - (PR #336) block — **not `evaluate()`** (RN doesn't use it). PR #336 is **CONFLICTING / REVIEW_REQUIRED**, - so its shape may still move. Mitigate: develop against a linked / `npm pack` build; keep the RN diff - isolated so the dependency bump is the only integration point. -2. **Two evaluation paths in `FlagsClient` (DECIDED D4 — keep two).** Precomputed serves from a decoded - `Map`; rules evaluate lazily via `evaluateRulesBasedConfiguration()`. Residual risk: divergent reason - codes / type checks — mitigated by the shared mapping helper, the explicit per-resolution - path-selection (Step 3/5), and tests. (The evaluator returns `FLAG_NOT_FOUND` itself; RN only adds the - own-property guard — R14.) -3. **Exposure/telemetry parity — BLOCKING (G4, CORRECTED D3, NARROWED).** Native tracks **every** successful - assignment (`doLog` gates only the exposure event; RUM + eval telemetry are separate), so RN must - `track` unconditionally on assignment. The rules `flagMetadata` **already carries** serialId - (`__dd_split_serial_id`) and timestamp (`__dd_eval_timestamp_ms`); **only `extraLogging` is missing** - — and RN's `FlagCacheEntry`/bridge have no serial/timestamp slot anyway. Define the exact native - payload, have upstream expose **`extraLogging`**, and confirm whether the `INTEGER`/`NUMERIC`→`number` - collapse matters for native telemetry. -4. **Bundle size (CORRECTED D5 — mostly a non-issue, two future adds).** The engine + `spark-md5` + the - rules evaluator **already ship** via RN's root-barrel import today, so the incremental rules cost is the - wire branch only. Two **future** upstream additions are genuinely new mass: the protobuf runtime (G2) - and a **synchronous SHA-256** for obfuscation (G11). Metro won't code-split, so dynamic import is out; a - provider/subpath split is the only real lever and only if measurement justifies it. Measure first. -5. **Hermes/JSC bundling.** `sharders.ts`/`spark-md5`, the future protobuf runtime (G2), and the future - **synchronous SHA-256** (G11) must run under **Hermes and JSC** across the supported RN range. Add smoke - tests + SHA-specific release-build perf/bundle measurements; confirm no Node `crypto` / browser-only Web - Crypto / unavailable globals. -6. **Context/logger threading + paradigm (item 5, CORRECTED C).** `resolveX` discards the OF context+logger - and evaluates against the stored context. The web SDK is **static-context** — there is no - client/invocation context; context is global/domain + `before`-hook mutations. Thread the resolver's - effective context+logger into rules eval **through a resolved dependency boundary (R13)**, settle the - `id`/`targetingKey` precedence (R15), and — see R12 — do the precomputed match against that - per-resolution context. -7. **Security / opt-in — justification splits by config source (D6).** For **Datadog-generated** configs - the platform per-flag switch is the opt-in (but the RFC is a first draft and scoped client tokens don't - exist yet, RFC:109). For **customer-supplied/bundled** wires — which this provider accepts — platform - controls are **bypassed**, so `setConfiguration(rules)` is the opt-in and the customer owns supplying - client-appropriate rules. "No SDK gate" still holds; keep sign-off on record + ship the docs caveat. -8. **Wire naming/version churn (§2.5).** `rulesBased` (code) vs `server` (RFC) vs `rules` (Confluence); - `version` 1 vs 2. The docs are drafts. Pin to the released version, never hard-code the field name, add a guard test. -9. **Obfuscation — design known, three hard prereqs, and today it fails **open** (G6/G11/G12, D7).** - Salted `ONE_OF_SHA256`/`NOT_ONE_OF_SHA256` operators, engine-evaluated. RN needs no detection/pre-hash, - **but**: (a) the operators are absent from 2.0.1 and an unknown operator **silently returns `DEFAULT`** - (not an error) — RN must reject unknown operators at load as `GENERAL` and upstream must add them + - capability/version negotiation; (b) SHA-256 must be **synchronous** and Hermes/JSC-safe — new bundle - mass (G11); (c) the salted-hash **protocol is unspecified** — needs cross-SDK test vectors (G12). - **Threat model:** salt defeats precomputation/rainbow tables but does **not** hide low-entropy guessable - values from offline enumeration (NIST SP 800-132) — do not call it "confidentiality". -10. **Untrusted-wire validation + regex — high (G7, CORRECTED F).** `evaluateRulesBasedConfiguration` - derefs `config.flags[flagKey]` before its try/catch and builds `new RegExp(...)` from wire patterns, - so a malformed/hostile rules wire can throw or hang from a READY provider. Structural validation on - load is doable in RN, but **ReDoS is not reliably detectable by validation alone** — require an - upstream safe-regex guarantee / static policy / bounded engine, run adversarial tests in an isolated - time-bounded process, and **clone before freezing** the snapshot (don't freeze the caller's object). -11. **Missing vs anonymous targeting key (G8).** `''` buckets as a real subject; only null/undefined - raises `TARGETING_KEY_MISSING`. RN's required-`string` type and missing `FlagErrorCode` member make - "missing" unrepresentable. Decide the semantics (D8) before coding the context adapter. -12. **Per-resolution path selection (B).** A `before` hook can mutate the resolution context, so a - precomputed-vs-rules choice frozen at reconcile time (against the stored context) could serve a - precomputed value to a non-matching context (assignment leak). Re-check the precomputed match against - the effective resolution context on every evaluation, or explicitly ignore/forbid hook context changes. -13. **OpenFeature type/dependency boundary (N, NEW).** `packages/core` has **no** `@openfeature/*` dep, and - flagging-core ships `.d.ts` that import `@openfeature/core` while declaring it only as a devDependency — - so the shared types resolve only by hoisting accident. Threading `Logger`/`ResolutionDetails` into core - would deepen this. Fix by either passing structurally-compatible internal types from - react-native-openfeature into core (no core dep), or adding an explicit core dep **and** getting - flagging-core to publish `@openfeature/core` as a real dependency. Also decide public-API vs internal - entry point for the threaded params. -14. **Prototype-unsafe flag lookup (item 1, NEW — bug).** `config.flags[flagKey]` on a plain object means an - absent flag named `toString`/`__proto__`/`constructor` returns `DISABLED` instead of `FLAG_NOT_FOUND`. - RN must own-property-guard before delegating, or get an upstream own-property/null-prototype fix. -15. **`id` vs `targetingKey` precedence (D9, NEW).** The evaluator lets a customer `id` attribute override - the targeting-key id for **rule matching** while **sharding** uses the targeting key — split identity. - Enforce a contract (recommended: reserve `id` for the targeting key, drop a customer `id`) and test it. -16. **Public config opacity is already partial (item 2, NEW).** `ParsedFlagsConfiguration` (= upstream - `FlagsConfiguration`) is exported from the package root, so its structure — `precomputed` today, - `rulesBased` after the bump — is inspectable/constructable by TS consumers regardless of whether the - sub-types are exported. Either soften the opacity claim (accept structural visibility, as precomputed - already is) or make `ParsedFlagsConfiguration` a branded/opaque type (breaking type-compat change). - ---- - -## 8. Decisions & remaining open questions - -> **Reviewed over six rounds (2026-07-22 → 07-23); all items confirmed accurate against installed -> `@datadog/flagging-core@2.0.1`, PR #336, native Android/iOS clients, and OpenFeature specs.** -> R1 reversed **D3/D5**, corrected §2 (engine already published), added **G7/G8/D8**, flagged **D6/D7**. -> R2: use the already-published **`evaluateRulesBasedConfiguration`** not `evaluate()` (D4/A); path -> selection **per-resolution** (R12/B); mixed-validity split parse-time vs structural (D); types internal -> (E); regex safety is an **upstream** contract (F); **Q3 narrowed to `extraLogging`** (H); obfuscation -> **undetectable → "unsupported"** (G/D7). R3: **`evaluateRulesBasedConfiguration` DOES return -> `FLAG_NOT_FOUND`** (my R2 note was wrong — corrected) but its lookup is **prototype-unsafe** (G9/bug); -> `ParsedFlagsConfiguration` is **already public** so opacity is only partial (**D10**, R16); the -> **`@openfeature/*` dependency boundary is unsound** (G10, R13); **`id` overrides targeting-key** for -> rule matching (**D9**, R15). R4 (Obfuscation RFC): obfuscation is **salted `ONE_OF_SHA256` operators + -> binary structure**, engine-evaluated and server-compatible — RN no-op — superseding the "customer -> pre-hashes"/"unsupported" framing. R5 (obfuscation deep-dive) tempered R4: those operators are **absent -> from 2.0.1 and today fail *open* to a silent `DEFAULT`** → must **reject unknown operators as `GENERAL`** -> + capability/version (G6); SHA-256 must be **synchronous/Hermes+JSC-safe = new bundle mass** (G11, -> corrects D5); the salted-hash **protocol is unspecified** → needs cross-SDK vectors (G12); "confidential" -> is **overstated** — salt stops precomputation but not offline enumeration of guessable values (D7 threat -> model); and the platform opt-in only covers **Datadog-generated** configs, not customer-supplied wires (D6). -> R6: unsupported-operator validation belongs **upstream** (`validateRulesConfiguration`/capabilities) or -> derived from the pinned `OperatorType` — **not** an RN-maintained set that drifts (G6); unsupported -> operators invalidate the **rules branch**, keeping a valid precomputed sibling (matrix in Step 3); -> malformed **SHA condition shapes** (salt/digest) need load-time rejection (G12); the "what stays visible" -> list must cover **all** UFC metadata incl. `extraLogging`/`doLog`/allocation keys/serialIds/salts (D7); -> "fail-open" → "**silent fallback**". -> **Upstream items (G1, G2, G4, G6, G7, G9, G10, G11, G12) are collaboration points with the Core -> developer** — `@datadog/flagging-core` is under active development, so edge-case gaps/bugs are expected. -> Note them here and work fixes through *together* — **not** external blockers to file or work around -> unilaterally. Raise with Core as the design firms up. - -### Decisions (2026-07-23) -- **D3 — Exposure/`doLog` (REVERSED):** call native `track` for **every successful assignment, - NOT gated on `doLog`** — matching the precomputed path (`FlagsClient.ts:441`, confirmed correct). - Native applies `doLog` to the *exposure event only*; RUM + evaluation telemetry fire independently - (verified in Android `trackResolution` / iOS `trackEvaluation`). Do not track for - disabled/unmatched/type-mismatch/not-found/error (J). **Blocked by G4:** the rules result omits - `extraLogging`, so the synthesized `FlagCacheEntry` is not yet faithful — resolve upstream first. (§Step 5, R3) -- **D4 — Evaluation paths (REFINED):** **keep two paths** — precomputed on the decoded `Map` + `track`; - rules via the **rules-only `evaluateRulesBasedConfiguration()`** (already in 2.0.1), called with the UFC - directly (A). RN selects the path itself (no combined `evaluate()`), so precomputed never routes through - upstream arbitration and `decodePrecomputedFlags` validation is never bypassed. One explicit, - **per-resolution** selection (Step 3/5, B) + a shared mapping helper. (§Step 3/6, R2/R12) -- **D5 — Bundle size (CORRECTED):** the engine + `spark-md5` + the rules evaluator **already ship** via - RN's existing root-barrel import, so enabling rules is a near-zero incremental cost (the wire branch - only). **Two future upstream additions are the real new mass:** the protobuf runtime (G2) and a - **synchronous SHA-256** for obfuscation (G11). **Measure separate baselines.** Dynamic `import()` - rejected (Metro doesn't code-split). A precomputed-only provider + `@datadog/flagging-core/precomputed` - subpath is the only real lever (needs an upstream subpath 2.0.1 lacks + an RN `wire.ts` rework) — pursue - only if measurement demands it. (§Step 6, R4) -- **D6 — Security/opt-in (split the justification by config provenance).** "No additional SDK gate" still - holds, but the *why* differs by source, and the platform argument does **not** cover arbitrary offline - wires: - - **Datadog-generated configs:** the platform enforces distribution policy — a **per-flag switch** - governs whether a flag may be used in rules-based client eval, so the config only carries opted-in - rules. This is the "explicit opt-in" the Offline-Init RFC wants, satisfied server-side. *Caveats:* the - Obfuscation RFC is a first draft (per-flag vs per-org debated), and **scoped client tokens don't exist - today** (RFC:109) — so the fetch-time token control is partly aspirational for the rules case. - - **Customer-supplied / bundled wires (this offline provider accepts these):** they **bypass** Datadog's - generation and token controls entirely, so the platform opt-in doesn't apply. Here the opt-in is simply - **calling `setConfiguration(rules)`**, and the **customer is responsible** for supplying - client-appropriate rules. - Keep product/security sign-off on record; ship the docs caveat (rules are on-device / reverse-engineerable) - and the "what stays visible" list (D7) regardless. (§Step 7, R7) -- **D7 — Obfuscation (design known; NOT yet supportable).** Obfuscation is **new salted operators** - (`ONE_OF_SHA256`/`NOT_ONE_OF_SHA256` from `ONE_OF`, server-compatible, engine-evaluated) + **binary - structure**. It is **not** a separate payload mode and needs **no** RN detection/rejection/context - pre-hashing (the engine hashes the subject attribute with the per-condition salt) — this supersedes the - earlier "customer pre-hashes" / "reject obfuscated" framing. **But three hard upstream prerequisites gate - "supported":** the SHA operators must exist (G6 — today they silently **fall back** to `DEFAULT`), backed by - a **synchronous** Hermes/JSC-safe SHA-256 (G11 — new bundle mass), against a **fully specified portable - hash protocol with cross-SDK test vectors** (G12). RN work: map the operators through once they exist, - **reject unknown operators at load as `GENERAL`** (don't serve a silent default), and verify with the - canonical vectors — not just "strings pass through". - - **Threat model (CORRECTED — do not call this "confidentiality"):** a public per-condition salt + one - fast SHA-256 defeats **cross-config precomputation / rainbow tables** and **obscures literal values**, - but does **not** protect **low-entropy / guessable** values — anyone with the bundled config can hash - candidates (`true`/`off`, common plans, domains, email dictionaries) against the included salt (NIST - SP 800-132: known-salt lets an attacker enumerate likely candidates; only added work slows dictionary - attacks). State it precisely; do not claim guessable values are hidden. - - **What stays visible** (document for customers — Step 8; **all** shipped UFC data per `ufc-v1.d.ts`): - flag/variant/attribute **names**; variant **values**; regex/numeric/version operands; decodable config - **structure**; **guessable hashed membership values**; **plus** allocation **keys**, split - **serialIds**, **`extraLogging`**, **`doLog`**, allocation `startAt`/`endAt`, shard **salts**, - **environment name**, **`createdAt`**. (G6/G11/G12, R9) -- **D8 — Targeting key (NEW):** decide whether missing (`undefined`) and anonymous-empty (`''`) are - distinct. `''` buckets as a real subject; only null/undefined yields `TARGETING_KEY_MISSING`. If - distinct, thread `undefined` through the rules path and add `TARGETING_KEY_MISSING` to `FlagErrorCode`; - if not, document that all keyless contexts share one bucket. (§Step 4/7, G8, R11) -- **D9 — `id` vs `targetingKey` (NEW):** the engine lets a customer `id` attribute override the - targeting-key id for **rule matching** while **sharding** uses the targeting key. **Recommended:** - reserve `id` for the targeting key — drop a customer-supplied `id` in the flat adapter so matching and - bucketing share one subject id. Assert in tests. (§Step 5, R15) -- **D10 — Config opacity (NEW):** `ParsedFlagsConfiguration` is already public and structurally exposes - `precomputed` (today) and `rulesBased` (post-bump). Choose: **(a) accept structural visibility and drop - the "opaque" language** (pragmatic — matches how precomputed already ships), or **(b) brand - `ParsedFlagsConfiguration`** as opaque (breaking type-compat change). Not exporting the sub-types is - necessary but **not sufficient** for opacity. (§Step 1/8, R16) -- **D11 — OpenFeature dependency boundary (NEW):** do not import `@openfeature/*` bare into `packages/core`. - Choose: **(a)** keep OF types in react-native-openfeature and pass structurally-compatible internal - context/logger types into `FlagsClient` (no core dep — preferred, and keeps threading off the public - `get*Details` API via an internal entry point), or **(b)** add an explicit core `@openfeature/*` dep and - require flagging-core to declare `@openfeature/core` as a real (non-dev) dependency. (§Step 5, G10, R13) - -### Remaining open questions (PUNTED — revisit with flagging-core owners; do not block planning) -- [ ] **Q1 (punted):** which published `@datadog/flagging-core` version adds the `rulesBased` wire parsing - + parsed-config slot; confirm `configurationFromString` populates the rules branch and the final wire - field name/version. (`evaluateRulesBasedConfiguration` + `UniversalFlagConfigurationV1` already ship.) (G1/G3/R8) -- [ ] **Q2 (punted):** protobuf rules encoding — when flagging-core publishes the `.proto` and switches - `response` from JSON to protobuf/base64-decode; which protobuf runtime, and is it Hermes-safe. (G2/§2.5) -- [ ] **Q3 (narrowed):** will upstream expose **`extraLogging`** on the rules-eval result (serialId + - timestamp are already in `flagMetadata`, and RN's bridge has no slot for them anyway), and does native - telemetry need the original `INTEGER`/`NUMERIC` type (engine collapses both to `number`)? (G4/D3 — blocking) diff --git a/dynamic_offline_pr_stack.plan.md b/dynamic_offline_pr_stack.plan.md index ae3ab3491..c12d63299 100644 --- a/dynamic_offline_pr_stack.plan.md +++ b/dynamic_offline_pr_stack.plan.md @@ -18,8 +18,9 @@ Keep all three pull requests in draft state. ## Temporary upstream code -The published flagging-core package does not contain the final rules wire contract. -It also does not contain all required validation and tracking metadata. +Published flagging-core version 2.0.2 does not contain the new rules wire contract. +Upstream PR #344 adds the protobuf rules parser, SHA-256 evaluation, validation, and React Native compatibility. +Upstream PR #336 uses that parser in the browser `CoreProvider`. Put a `TODO` immediately before each temporary implementation. The `TODO` must identify the upstream replacement. @@ -27,17 +28,31 @@ Do not hide temporary behavior in a general helper. Tests can use a fake rules engine. Production code must use one internal engine adapter. +Remove temporary JSON rules-wire parsing and duplicate rules validation after the upstream package is published. +Do not wait for upstream `extraLogging`. +The field is deprecated. +Use an empty object only where the current Android bridge requires it. + ## PR1 — Rules configuration and engine boundary Add the internal boundary for the rules engine. +- Bump to the flagging-core release that contains PR #344. +- Use a packed PR #344 package before publication. +- Use `FlagsConfiguration.rules.response`. +- Remove the temporary `rulesBased` and JSON compatibility shapes. +- Remove duplicate structural validation after the dependency bump. - Add internal rules configuration types. - Add a rules-engine adapter. - Convert SDK contexts to engine contexts. - Normalize engine results. -- Validate rules before storage. -- Clone valid rules before storage. +- Use the upstream protobuf rules object. +- Use upstream parser validation. +- Add an own-property guard for flag lookup until upstream fixes it. +- Keep regular-expression safety as an explicit open item. - Add adapter contract tests. +- Add a protobuf wire contract test. +- Confirm that rules serialization throws. - Add fake-engine test helpers. - Keep current provider behavior unchanged. - Keep precomputed evaluation unchanged. @@ -57,11 +72,16 @@ Add dynamic evaluation to `FlagsClient`. - Use matching precomputed data first. - Use valid rules data second. - Return the applicable error when neither path is usable. +- Treat a flag that the upstream parser drops as `FLAG_NOT_FOUND`. +- Keep other valid rules flags. - Map rules results to `FlagDetails`. -- Track only successful rules assignments. +- Convert successful rules results to `TrackableAssignment`. +- Track each successful rules assignment through the current native bridge. +- Let native code apply `doLog` to exposure events. +- Do not require split serial ID or evaluation timestamp in the mobile exposure payload unless the mobile contract changes. - Keep online and precomputed behavior unchanged. - Add rules-only and mixed-configuration tests. -- Use the fake engine for state-matrix tests. +- Use the fake engine for path-selection tests. The main review question is: @@ -76,11 +96,14 @@ Expose dynamic evaluation through the existing offline provider. - Keep `initialize` network-free. - Keep `onContextChange` network-free. - Keep current provider event mapping. -- Add hook-context tests. +- Add global-context and domain-context tests. +- Confirm the Web SDK 1.8 hook-context constraint. - Add real-provider integration tests. +- Use a protobuf rules wire in integration tests and examples. - Update the provider documentation. - Update both example applications. -- Add compatibility and bundle checks where the repository supports them. +- Add Hermes and JSC checks where the repository supports them. +- Record the upstream protobuf bundle-size measurement. The main review question is: diff --git a/dynamic_offline_simplified.plan.md b/dynamic_offline_simplified.plan.md index 699f420b2..c66d72ee1 100644 --- a/dynamic_offline_simplified.plan.md +++ b/dynamic_offline_simplified.plan.md @@ -6,7 +6,7 @@ Technical names and API names do not change. **Jira:** FFL-2837 **Base branch:** `blake.thomas/FFL-2666` **Work branch:** `blake.thomas/FFL-2837` -**Upstream reference:** DataDog/openfeature-js-client PR #336 +**Upstream references:** DataDog/openfeature-js-client PRs #343, #344, and #336 ## Source documents @@ -15,8 +15,9 @@ Technical names and API names do not change. - The ConfigurationWire specification defines the intended protobuf and base64 format. - The Obfuscation RFC defines the proposed client-rules protection. -These documents are drafts. -Names, versions, and formats can change. +The RFC documents are drafts. +Upstream PR #344 now defines the expected implementation contract. +Names and versions can still change before publication. ## 1. Objective @@ -53,66 +54,64 @@ configurationFromString -> setConfiguration -> evaluate ## 2. Current `@datadog/flagging-core` state -### 2.1 Features in version 2.0.1 +### 2.1 Published version 2.0.2 -Version 2.0.1 already contains the rules engine. -It exports the engine from the package root. +PR #343 published flagging-core version 2.0.2. +This version removes an unnecessary `@datadog/js-core` dependency. +It does not contain the new rules wire features. -It contains these parts: - -- `evaluateForSubject` -- `evaluateRulesBasedConfiguration` -- Rule operators and rule matching -- Sharding and hashing -- UFC v1 types -- `TargetingKeyMissingError` -- Evaluation metadata -- MD5 utility functions -- The `spark-md5` dependency - -Version 2.0.1 has these configuration limits: - -- `FlagsConfiguration` contains only `precomputed`. -- The wire parser reads only precomputed data. -- The package has one root export. -- The package has no subpath exports. +Version 2.0.2 still contains the existing rules engine. +It exports `evaluateRulesBasedConfiguration` from the package root. +It also contains sharding, MD5, UFC v1 types, and `spark-md5`. The React Native wire module already imports the package root. -Metro does not remove the unused rules engine. -Thus, the current application bundle already contains the rules engine and `spark-md5`. - -### 2.2 Features in upstream PR #336 +Thus, the current application bundle already contains the existing rules engine. -PR #336 adds these core features: +### 2.2 Expected features from upstream PR #344 -- A `rulesBased` wire branch -- A `rulesBased` field in `FlagsConfiguration` -- A combined `evaluate` function +PR #344 is the required upstream dependency change. +It adds these features: -React Native does not need the combined `evaluate` function. -React Native selects the evaluation path itself. -React Native calls `evaluateRulesBasedConfiguration` for the rules path. +- The opaque `FlagsConfigurationWire` type +- A version `1` wire with a `rules` branch +- A `rules` field in `FlagsConfiguration` +- A protobuf and base64 rules response +- The checked-in UFC protobuf schema +- Protobuf-ES decoding +- Independent parsing of the precomputed and rules branches +- Per-flag validation and feature-level rejection +- `ONE_OF_SHA256` and `NOT_ONE_OF_SHA256` +- A synchronous JavaScript SHA-256 implementation +- An internal UTF-8 implementation +- A React Native Metro smoke test -The required dependency update must provide these two features: +The parser keeps a valid branch when the other branch is malformed. +The parser drops an unsupported or invalid rules flag. +It does not invalidate the complete rules branch. -- Rules wire parsing -- The parsed `rulesBased` field +The published package will expose `configuration.rules.response`. +Do not use the old planned name `rulesBased`. -PR #336 is open. -Its merge state is dirty. -It requires review. +### 2.3 Expected features from upstream PR #336 -### 2.3 Browser provider behavior +PR #336 is stacked on PR #344. +It adds the browser `CoreProvider` and a combined core `evaluate` function. +It proves that `configurationFromString` returns a rules object that the evaluator can use. -Use the browser `CoreProvider` as a reference. +Use the browser `CoreProvider` as an integration reference. -- A rules configuration is valid for any context. -- A precomputed mismatch is an error only when no rules fallback exists. -- `onContextChange` stores the new context. +- A rules configuration is valid for each compatible context. +- Matching precomputed data has priority. +- Rules data is the fallback after a precomputed mismatch. - `onContextChange` does not fetch. - `setConfiguration` emits the applicable provider event. -### 2.4 Evaluation path +React Native can continue to use its decoded precomputed `Map`. +It can call `evaluateRulesBasedConfiguration` only for the rules path. +This keeps current precomputed validation and native tracking behavior. +Do not adopt the combined `evaluate` function without a parity review. + +### 2.4 Evaluation and tracking path Select the path for each resolution. Do not select the path only during reconciliation. @@ -120,15 +119,16 @@ Do not select the path only during reconciliation. Use this order: 1. If precomputed data matches the effective context, use its decoded `Map`. -2. Otherwise, if valid rules data exists, evaluate the rules. +2. Otherwise, if rules data exists, evaluate the rules. 3. Otherwise, if precomputed data exists, return `INVALID_CONTEXT`. 4. Otherwise, return `PROVIDER_NOT_READY`. -The rules evaluator converts `targetingKey` to the `id` subject attribute. -It does this only when `targetingKey` is not null. +The protobuf evaluator preserves a missing targeting key. +It requires the key only when a shard uses the targeting key. +An empty string is a real key. The evaluator returns `ResolutionDetails`. -The metadata contains these applicable fields: +The metadata can contain these fields: - `allocationKey` - `variationType` @@ -138,118 +138,113 @@ The metadata contains these applicable fields: - `__dd_do_log` - `__dd_eval_timestamp_ms` -The metadata does not contain `extraLogging`. -This missing field blocks correct native tracking. +The split serial ID and evaluation timestamp are server-side tracing metadata. +The current Android and iOS mobile exposure APIs do not accept these fields. +The native mobile SDK creates the exposure timestamp. -The evaluator treats an empty targeting key as a real subject. -It raises `TARGETING_KEY_MISSING` only for a null or undefined key. +The `extraLogging` field is deprecated upstream. +Do not wait for flagging-core to return it. +Use an empty object only where the current Android bridge requires it. -The React Native `EvaluationContext` requires a string key. -Its documentation tells customers to use an empty string. -The React Native `FlagErrorCode` does not contain `TARGETING_KEY_MISSING`. +The rules evaluator is pure. +React Native must call the existing native tracking bridge after each successful assignment. +Native code applies `doLog` to exposure events. +Native code also applies its RUM and evaluation settings. -## 2.5 Wire format +### 2.5 Wire format and compatibility -The source documents use different wire formats. +Use the PR #344 contract: -- The ConfigurationWire specification uses version `1`, field `rules`, and protobuf/base64. -- PR #336 uses version `1`, field `rulesBased`, and JSON. -- The Portable Flag Configuration RFC uses version `2`, field `server`, and JSON. - -The intended rules response uses protobuf and base64. -The current upstream code uses JSON. -Do not depend on the JSON format. - -The Obfuscation RFC requests a binary format. -This request does not select protobuf by itself. -The ConfigurationWire decision selects protobuf. - -The protobuf schema must support these new operators: - -- `ONE_OF_SHA256` -- `NOT_ONE_OF_SHA256` - -The schema must also support the salt fields. +- Wire version `1` +- Field `rules` +- A protobuf rules response encoded as base64 +- Raw-byte SHA salts +- Raw 32-byte SHA digests +- `sha256(salt || UTF8(String(attributeValue)))` Keep all decoding in `@datadog/flagging-core`. Do not add a rules decoder to React Native. +Do not serialize a decoded rules configuration. +PR #344 makes `configurationToString` throw when rules are present. -A future protobuf runtime will add bundle size. -A future synchronous SHA-256 implementation can also add bundle size. -Test both additions with Hermes and JSC. +PR #344 adds Protobuf-ES to the bundle. +Its browser measurement reports an increase of 6,229 bytes minified and 2,070 bytes compressed. +Its React Native smoke test bundles Android and iOS with React Native 0.76.9. +The test runs the Android bundle under Node without `TextEncoder`, `TextDecoder`, or `BigInt`. +It does not run the bundle in Hermes or JSC. -Publish the `.proto` schema before the protobuf migration. -Pin React Native to a released flagging-core version. -Verify the final field names and versions after publication. -Do not hard-code the wire field name in React Native. +Pin React Native to the released flagging-core version that contains PR #344. +Verify the final version and package exports after publication. ## 3. Upstream gaps ### G1 — Rules wire parsing and parsed field -**Status:** Blocking. - -Merge and publish the upstream rules wire changes. -Bump `@datadog/flagging-core` in `packages/core/package.json`. -Do not add the dependency to `packages/react-native-openfeature`. +**Status:** Implemented in PR #344. Publication is pending. -Use a linked package or an `npm pack` package during development. +Use `FlagsConfiguration.rules.response`. +Do not use `rulesBased`. +Use an `npm pack` package during development. +After publication, bump `@datadog/flagging-core` in `packages/core/package.json`. ### G2 — Protobuf rules response -**Status:** Upstream release contract. +**Status:** Mostly implemented in PR #344. -Publish the `.proto` schema. -Change the rules parser from JSON to protobuf. -Test the protobuf runtime with Hermes and JSC. +PR #344 includes the schema and the Protobuf-ES parser. +React Native must use the opaque parser. +Do not parse protobuf in this repository. -React Native must continue to use the opaque parser. +The raw `.proto` file is not part of the current npm package file list. +Confirm whether repository publication is sufficient. +Run the packed package in Hermes and JSC. ### G3 — Root exports -**Status:** Mostly complete. +**Status:** Implemented. Publication is pending. -Version 2.0.1 exports these required symbols: - -- `UniversalFlagConfigurationV1` -- `evaluateRulesBasedConfiguration` - -Verify that the new release populates the `rulesBased` field. +The package root exports the parser and evaluator. +PR #344 populates `rules`. +PR #336 adds the optional combined `evaluate` function. ### G4 — Native tracking metadata -**Status:** Blocking. +**Status:** React Native adapter work. It is not an upstream blocker. -The evaluator already returns the split serial ID and evaluation timestamp. -React Native cannot send those fields through its current bridge. +The current native bridge accepts a synthesized successful assignment. +It already sends the fields that the current mobile exposure APIs use: -The evaluator does not return `extraLogging`. -React Native needs this field to build `FlagCacheEntry`. +- Flag key +- Allocation key +- Variation key +- Value +- Reason +- `doLog` +- Evaluation context -Define the exact native tracking payload. -Then, select one upstream solution: +The evaluator does not log by itself. +Call the existing bridge after each successful rules assignment. +Do not stop the bridge call when `doLog` is false. +Native code applies the exposure policy. -- Add `extraLogging` to the rules result. -- Add a rules-tracking API. +Use an internal `TrackableAssignment` type. +Do not use `FlagCacheEntry` as the rules result type. +Use an empty `extraLogging` object for Android bridge compatibility. -Also verify the required variation type. -The evaluator converts `INTEGER` and `NUMERIC` to `number`. -The precomputed path keeps the original distinction. +Confirm with the mobile and backend owners that rules exposures use the current mobile event contract. +If they require split serial ID, evaluator timestamp, or error evaluations, add a new native tracking API. ### G5 — Bundle size and JavaScript engine support -**Status:** Low for current code. - -The rules engine and `spark-md5` already ship in the bundle. -The rules wire branch adds little current bundle size. +**Status:** Measurement exists. Runtime verification remains. -Two future changes can add significant code: +PR #343 removes an unnecessary dependency. +PR #344 measures the browser protobuf cost. +PR #344 also adds a React Native Metro smoke test. -- The protobuf runtime -- A synchronous SHA-256 implementation - -Measure each change. -Test each change with Hermes and JSC. +Measure the packed dependency in this repository. +Run the rules flow in Hermes and JSC. +Test the supported React Native version range. Do not use a dynamic import. Metro does not create a smaller release bundle from this import. @@ -259,58 +254,27 @@ This split requires a flagging-core subpath export. ### G6 — Unsupported obfuscation operators -**Status:** Blocking. - -Version 2.0.1 does not support the SHA-256 operators. -An unknown operator makes the current evaluator return `DEFAULT`. -The evaluator does not return an error. -This silent fallback can return the wrong value. - -Do not maintain a separate operator list in React Native. -Use one of these preferred upstream solutions: - -1. Export `validateRulesConfiguration`. -2. Export evaluator capabilities. -3. Return `GENERAL` for an unsupported operator. -4. Reject the operator in the parser. - -If React Native needs a temporary check, use the pinned `OperatorType` enum. -Do not create a second list. - -Define capability ownership. +**Status:** Implemented with different semantics in PR #344. -- An official fetch request must advertise evaluator capabilities. -- The service must omit or reject unsupported flags. -- A portable wire must state its required capabilities. -- Alternatively, the parser must preserve and reject unknown operators. +PR #344 supports the SHA-256 operators. +The protobuf schema contains a per-flag minimum feature level. +The parser drops an unsupported or invalid flag. +It keeps other valid rules flags. -An unsupported operator invalidates the rules branch. -It does not always invalidate a valid precomputed branch. - -Use this state matrix: - -- Rules only and unsupported operator: return `GENERAL`. -- Matching precomputed data and unsupported rules: serve precomputed data. -- Mismatched precomputed data and unsupported rules: return `GENERAL`. -- Hook context falls through to unsupported rules: return `GENERAL` for that resolution. - -Keep the provider `READY` while matching precomputed data is usable. -Invalidate the complete rules branch for one unsupported operator. -Do not isolate an unsupported operator to one flag in this release. +Do not add a second operator list in React Native. +Do not invalidate the complete rules branch. +For a dropped flag, return `FLAG_NOT_FOUND`. +Matching precomputed data still has priority. ### G7 — Untrusted rules and regular expressions -**Status:** High risk. +**Status:** Structural validation is mostly implemented. Regular-expression safety remains. -The evaluator reads `config.flags[flagKey]` before its `try` block. -A malformed UFC can throw an exception. +PR #344 validates the protobuf structure. +It drops invalid rules flags. +It rejects malformed indexes, values, ranges, and hashes. -The rules engine creates regular expressions from wire values. -A hostile expression can block the JavaScript thread. - -Validate the rules snapshot before storage. -Validate the envelope, flags, allocations, splits, variations, and shard ranges. -Validate reserved property names. +Do not duplicate this validator in React Native after publication. Do not claim that structural validation stops ReDoS. Select one regular-expression protection: @@ -322,31 +286,25 @@ Select one regular-expression protection: Run hostile-expression tests in a separate process. Set a time limit for that process. -Clone the rules snapshot before you freeze it. -Do not freeze the caller's object. +Treat the parsed configuration as immutable. +Add mutation tests for the public configuration object. ### G8 — Missing targeting key -**Status:** Decision required. - -Decide if a missing key and an empty key are different. +**Status:** Decided in PR #344. -If they are different: - -- Preserve `undefined` for the rules path. -- Relax the internal context type. -- Add `TARGETING_KEY_MISSING` to `FlagErrorCode`. - -If they are not different: - -- Use the empty string. -- Document that all keyless contexts use one subject bucket. +Missing and empty targeting keys are different. +Preserve `undefined` for the rules path. +An empty string is a real key. +Return `TARGETING_KEY_MISSING` only when a shard requires a missing key. +Relax the internal context type and error union. ### G9 — Prototype-unsafe flag lookup -**Status:** Bug. +**Status:** Open upstream bug. -The evaluator uses `config.flags[flagKey]`. +Both upstream rules evaluators use an unsafe object lookup. +The combined evaluator in PR #336 also uses an unsafe precomputed lookup. A missing reserved key can resolve through `Object.prototype`. Examples include: @@ -355,13 +313,12 @@ Examples include: - `__proto__` - `constructor` -The evaluator can return `DISABLED` instead of `FLAG_NOT_FOUND`. +The protobuf evaluator can return `TYPE_MISMATCH` instead of `FLAG_NOT_FOUND`. Use an own-property check before evaluation. -Alternatively, fix the lookup in flagging-core. -A null-prototype dictionary is also acceptable. - -The precomputed path already uses a `Map`. +Request the upstream fix in PR #344 and PR #336. +Keep a React Native guard until the released dependency contains the fix. +The React Native precomputed cache already uses a `Map`. ### G10 — OpenFeature type dependency @@ -384,85 +341,62 @@ Prefer a separate internal entry point. ### G11 — Synchronous SHA-256 -**Status:** Blocking upstream work. - -Flag evaluation is synchronous. -Web Crypto `SubtleCrypto.digest` is asynchronous. -Do not use it in the synchronous evaluation path. - -Use a synchronous SHA-256 implementation. -The implementation must work with Hermes and JSC. -It must work across the supported React Native versions. +**Status:** Implemented in PR #344. Runtime verification remains. -Do not depend on Node `crypto`. -Do not depend on a browser-only global. -Do not depend on an unavailable runtime global. +PR #344 adds a synchronous JavaScript SHA-256 implementation. +It uses `Uint8Array` and `DataView`. +It does not use Node crypto, Web Crypto, or a browser-only API. -Measure bundle size and evaluation time in release builds. +Run it in Hermes and JSC. +Measure evaluation time in release builds. ### G12 — Portable salted-hash protocol -**Status:** Blocking upstream contract. +**Status:** Mostly defined in PR #344. -The Obfuscation RFC does not fully define the hash protocol. -Define these items: +PR #344 defines these items: -- Salt length -- Salt encoding -- Salt and value order -- Input framing +- Salt as raw protobuf bytes +- Digest as raw 32-byte values +- Salt before the attribute value +- Direct concatenation without a separator - UTF-8 encoding -- Unicode normalization -- Digest encoding -- Hexadecimal letter case -- Base64 padding, if applicable -- Number conversion -- Boolean conversion -- Empty-string behavior -- Null and missing-attribute behavior +- JavaScript string conversion for primitive values +- False for null or missing attributes - `NOT_ONE_OF_SHA256` behavior -Publish canonical cross-SDK test vectors. -The generator and all evaluators must produce the same bytes. +The parser validates the digest length. +It does not define a salt length or reject an empty salt. +It does not apply configuration-size, condition-count, or value-count limits. +It does not publish canonical cross-SDK protocol vectors. -Validate each SHA condition before the provider becomes `READY`. -Reject these conditions: - -- Missing salt -- Malformed salt -- Excessively large salt -- Incorrect salt length -- Incorrect salt encoding -- Incorrect digest length -- Incorrect digest encoding -- Non-string digest values -- Malformed condition shape -- Excessive condition count -- Excessive value count +Confirm the salt policy. +Add size limits. +Publish cross-SDK vectors. ## 4. React Native implementation ### Step 0 — Complete prerequisites - [ ] Publish flagging-core with rules wire parsing. -- [ ] Publish the parsed `rulesBased` field. -- [ ] Publish the required SHA operators before obfuscation support. -- [ ] Define and publish the salted-hash protocol. -- [ ] Provide a synchronous SHA-256 implementation. -- [ ] Resolve the `extraLogging` tracking requirement. +- [ ] Publish the parsed `rules` field. +- [ ] Publish the SHA operators and synchronous SHA-256 implementation. +- [ ] Confirm the remaining salt and size-limit rules. +- [ ] Confirm the current mobile exposure contract. +- [ ] Fix prototype-unsafe lookup upstream or keep the local guard. +- [ ] Select a regular-expression safety policy. - [ ] Bump `@datadog/flagging-core` in `packages/core`. - [ ] Update `yarn.lock`. - [ ] Verify all final field names, versions, and exports. -- [ ] Record the exact post-SHA flagging-core version. +- [ ] Record the exact flagging-core version. ### Step 1 — Define the parsed configuration type Do not export a named rules or UFC type. -Use `UniversalFlagConfigurationV1` only inside the flags implementation. `ParsedFlagsConfiguration` is already public. It is an alias of upstream `FlagsConfiguration`. -The new upstream type will expose `rulesBased.response`. +The new upstream type exposes `rules.response`. Select one API policy: @@ -477,14 +411,15 @@ Do not claim that the type is opaque unless you enforce opacity. Do not add React Native parsing code. Re-export the upstream conversion functions. -Add a round-trip test for a rules wire. +Add a parser test for a rules wire. Use the encoding from the pinned upstream version. +Confirm that `configurationToString` rejects a configuration that contains rules. ### Step 3 — Load and validate the configuration Keep the complete parsed `FlagsConfiguration`. Decode precomputed flags one time into a `Map`. -Keep a validated rules snapshot. +Keep the parsed protobuf rules object. Use this path order: @@ -495,30 +430,14 @@ Use this path order: Select the path for each resolution. Do not freeze the selection during reconciliation. -Treat parse failures and validation failures differently. - -For a parse failure: - -- The current parser returns an empty object. -- React Native cannot recover the valid sibling branch. -- Return `GENERAL`. - -For a decoded validation failure: - -- Validate each branch separately. -- Keep a valid sibling branch. -- Mark the invalid branch as unusable. -- Do not use the invalid branch as a fallback. - -Validate rules before the precomputed-only guard. -Do not store an unvalidated UFC. +The upstream parser validates each wire branch independently. +Keep a valid sibling when the other branch is malformed. +Return `GENERAL` only when the parser returns no usable branch. -Use the G6 state matrix for unsupported operators. -Use upstream validation when it is available. -If necessary, derive temporary validation from pinned `OperatorType`. - -Validate all SHA condition fields. -Apply the size limits from G12. +Do not add a second structural rules validator. +Trust the parser to drop unsupported or malformed rules flags. +Keep the own-property guard from G9 until upstream contains the fix. +Apply the size policy from G12 after the policy is defined. ### Step 4 — Reconcile the context @@ -526,7 +445,8 @@ For valid rules, accept every external context. Set `configurationStatus` to `ready`. Do not fill `flagsCache` for rules. -Do not create an empty targeting key until D8 is complete. +Preserve a missing targeting key. +Do not replace it with an empty string. Keep the precomputed behavior. Return `INVALID_CONTEXT` only when no usable rules fallback exists. @@ -553,10 +473,11 @@ Pass the resolution logger. The web SDK uses the static-context model. It has no invocation context. The effective context comes from global or domain state. -A `before` hook can change this context for one resolution. +Web SDK 1.8 freezes the hook context. +A `before` hook cannot replace the resolution context. Check the precomputed context for every resolution. -This check prevents an assignment leak after a hook changes context. +This check keeps the path decision correct for the effective context. Resolve the OpenFeature dependency boundary before implementation. Prefer compatible internal types and an internal `FlagsClient` method. @@ -577,10 +498,12 @@ Do not create this error again in React Native. Add an own-property check for the flag key. Return `FLAG_NOT_FOUND` for an absent reserved-name key. +Convert a successful rules result to an internal `TrackableAssignment`. Track every successful assignment through the native bridge. Do not use `doLog` to stop the bridge call. Native code uses `doLog` only for the exposure event. RUM and evaluation telemetry use separate settings. +Use an empty `extraLogging` object for Android bridge compatibility. Track only a real assigned variant. Do not track these results: @@ -592,8 +515,6 @@ Do not track these results: - `FLAG_NOT_FOUND` - Error results -Do not implement tracking until G4 is complete. - Keep the online cache path unchanged. Keep the precomputed cache path unchanged. @@ -607,7 +528,8 @@ Do not pass precomputed data to the rules evaluator. Do not bypass `decodePrecomputedFlags`. Create one shared result-mapping helper. -Use it to build `FlagDetails` and `FlagCacheEntry`. +Use it to build `FlagDetails` and `TrackableAssignment`. +Do not put a rules result in the precomputed cache. Measure these bundle baselines separately: @@ -630,7 +552,8 @@ Keep `onContextChange` network-free. For valid rules, reconciliation returns `ready`. Do not use the precomputed mismatch error for valid rules. -Resolve D8 before you define empty-context behavior. +Preserve an empty provider context. +Return `TARGETING_KEY_MISSING` only when the selected rule needs a targeting key. Update the class comment. Remove the precomputed-only instruction that forbids `setContext`. @@ -684,7 +607,7 @@ Document all data that remains visible: - Allocation keys - Split serial IDs - `doLog` -- `extraLogging` +- Precomputed `extraLogging`, when present - Environment metadata - Timestamps - Salts @@ -701,18 +624,14 @@ Add this value import from `@datadog/flagging-core`: - `evaluateRulesBasedConfiguration` -Add this internal type import: - -- `UniversalFlagConfigurationV1` - Keep the existing wire and precomputed imports. +Derive the rules response type from `FlagsConfiguration['rules']`. Do not import OpenFeature types directly into core unless D11 selects that policy. Prefer compatible internal context and logger interfaces. Use an internal `FlagsClient` entry point. -Do not export `ParsedRulesBasedConfiguration`. -Do not export `UniversalFlagConfigurationV1`. +Do not export a named rules configuration type. Add one internal context adapter. Convert between these forms: @@ -723,20 +642,20 @@ OpenFeature: { targetingKey, ...attributes } ``` No new native API is required. -The existing tracking bridge accepts a synthesized flag object. -G4 must supply the missing tracking metadata. +The existing tracking bridge accepts `TrackableAssignment`. +Add a native API only if the confirmed mobile contract requires more fields. ## 6. Test plan ### 6.1 Wire and configuration tests -- [ ] Parse a rules wire into `rulesBased.response`. -- [ ] Serialize the parsed rules configuration. +- [ ] Parse a rules wire into `rules.response`. +- [ ] Confirm that rules serialization throws. - [ ] Parse a wire with both branches. - [ ] Return an empty configuration for malformed wire data. - [ ] Return `GENERAL` when the loaded configuration is empty. - [ ] Detect a changed upstream field name or version. -- [ ] Test the protobuf format after it becomes available. +- [ ] Test the published protobuf fixture. - [ ] Verify unknown protobuf enum behavior. ### 6.2 Load and reconciliation tests @@ -748,8 +667,9 @@ G4 must supply the missing tracking metadata. - [ ] Use matching precomputed data before rules. - [ ] Use rules after a precomputed mismatch. - [ ] Return `INVALID_CONTEXT` for a precomputed-only mismatch. -- [ ] Apply the unsupported-operator state matrix. -- [ ] Keep valid precomputed data when the rules branch is invalid. +- [ ] Return `FLAG_NOT_FOUND` for a flag that upstream drops. +- [ ] Keep valid flags when upstream drops one unsupported flag. +- [ ] Keep valid precomputed data when the rules branch is malformed. ### 6.3 Rules evaluation tests @@ -762,17 +682,19 @@ G4 must supply the missing tracking metadata. - [ ] Return `DISABLED` for a disabled flag. - [ ] Return `DEFAULT` when no allocation matches. - [ ] Return `DEFAULT` when no variant exists. -- [ ] Test allocations before `startAt`. -- [ ] Test allocations at or after `endAt`. -- [ ] Test the selected missing-targeting-key policy. +- [ ] Test time partitions before and after each range boundary. +- [ ] Return `TARGETING_KEY_MISSING` when a shard requires a missing key. +- [ ] Evaluate without a targeting key when the selected rule does not need it. - [ ] Treat an empty string as a real targeting key. ### 6.4 Per-resolution context tests - [ ] Load matching precomputed data and rules data. -- [ ] Change one resolution context with a `before` hook. -- [ ] Confirm that the resolution does not use mismatched precomputed data. -- [ ] Confirm that the resolution uses rules or returns the applicable error. +- [ ] Change the global context. +- [ ] Change a domain context. +- [ ] Confirm that a mismatched precomputed branch is not used. +- [ ] Confirm that rules are used after the mismatch. +- [ ] Confirm Web SDK 1.8 hook-context behavior. ### 6.5 Tracking tests @@ -781,7 +703,9 @@ G4 must supply the missing tracking metadata. - [ ] Include the variation key. - [ ] Include the allocation key. - [ ] Include the string variation value. -- [ ] Include `extraLogging`. +- [ ] Include an empty `extraLogging` object for Android compatibility. +- [ ] Confirm that native code emits an exposure only when `doLog` is true. +- [ ] Confirm that native RUM and evaluation settings still apply. - [ ] Do not track `DISABLED`. - [ ] Do not track unmatched `DEFAULT`. - [ ] Do not track `FLAG_NOT_FOUND`. @@ -790,28 +714,27 @@ G4 must supply the missing tracking metadata. ### 6.6 Validation and security tests -- [ ] Reject a missing `flags` map. -- [ ] Reject a malformed flag. -- [ ] Reject a bad shard range. +- [ ] Use upstream fixtures for malformed protobuf data. +- [ ] Confirm that the parser drops a malformed flag. +- [ ] Confirm that the parser rejects a bad shard range. - [ ] Test inherited property names. - [ ] Test mutation of the source object after load. -- [ ] Clone the source object before freezing. - [ ] Run a hostile regex in an isolated process. - [ ] Stop the hostile-regex process at its time limit. - [ ] Verify the selected ReDoS protection. -- [ ] Reject an unsupported operator as `GENERAL`. -- [ ] Do not return a silent `DEFAULT` for an unsupported operator. +- [ ] Confirm that an unsupported flag becomes `FLAG_NOT_FOUND`. +- [ ] Confirm that an unsupported flag does not cause a silent `DEFAULT`. - [ ] Test a newer cached configuration with an older evaluator. -- [ ] Reject malformed SHA salts and digests. +- [ ] Reject malformed SHA digests. +- [ ] Apply the selected empty-salt policy. - [ ] Reject oversized SHA conditions. ### 6.7 Mixed-configuration tests -- [ ] Confirm that one parse failure currently removes both branches. -- [ ] Confirm that React Native cannot recover a sibling after this parse failure. -- [ ] Isolate a decoded structural failure to its branch. -- [ ] Keep a valid sibling branch. -- [ ] Return `GENERAL` when the active path uses an invalid rules branch. +- [ ] Keep valid precomputed data when rules protobuf is malformed. +- [ ] Keep valid rules data when precomputed JSON is malformed. +- [ ] Parse both valid branches from one wire. +- [ ] Return `GENERAL` when neither branch is usable. ### 6.8 Obfuscation tests @@ -857,8 +780,8 @@ G4 must supply the missing tracking metadata. ### R1 — Unpublished upstream configuration support -PR #336 is not merged. -Its API can change. +PR #344 and PR #336 are not published. +Their APIs can change. Keep the React Native integration small. Use one dependency update as the integration point. @@ -872,15 +795,15 @@ Test the selection order. ### R3 — Tracking parity -The rules result does not contain `extraLogging`. -Do not ship incomplete tracking. -Resolve G4 first. +The rules evaluator does not produce a native assignment. +The React Native adapter must call the current native bridge. +Confirm that the current mobile exposure contract is sufficient. ### R4 — Bundle size The current engine already ships. -Protobuf and synchronous SHA-256 are future additions. -Measure both additions. +PR #344 adds Protobuf-ES and synchronous SHA-256. +Measure the packed dependency in this repository. ### R5 — Hermes and JSC support @@ -907,25 +830,28 @@ Add a contract test. ### R9 — Obfuscation -The design is not supportable until G6, G11, and G12 are complete. +PR #344 implements the required operators and SHA-256 function. +The remaining protocol limits and cross-SDK vectors are in G12. Do not describe salted SHA-256 as confidentiality. It does not protect guessable values from offline enumeration. ### R10 — Untrusted input and ReDoS -Malformed rules can throw. +The upstream parser rejects malformed protobuf flags. Hostile regex data can block the JavaScript thread. -Validate the snapshot and select a regex protection. +Select a regex protection. ### R11 — Missing targeting key -The current React Native type cannot represent a missing key. -Complete D8 before you implement the adapter. +The public React Native type requires a targeting key. +The internal rules context must permit a missing key. +Do not synthesize an empty key. ### R12 — Per-resolution path selection -A hook can change one resolution context. -Check precomputed compatibility after this change. +Global and domain contexts can change. +Select the path with the effective resolution context. +Do not claim that a Web SDK 1.8 hook can replace this context. ### R13 — OpenFeature dependency @@ -954,7 +880,9 @@ Complete D10 before you claim that the type is opaque. Call native tracking for every successful assignment. Do not stop the bridge call when `doLog` is false. Do not track default or error results. -Complete G4 before implementation. +Use `TrackableAssignment`. +Use an empty `extraLogging` object for Android compatibility. +Use the current native API unless the mobile contract review requires more fields. ### D4 — Evaluation paths @@ -966,7 +894,7 @@ Select the path for each resolution. ### D5 — Bundle size The current rules engine already ships. -Measure protobuf and synchronous SHA-256 as separate future additions. +Measure the PR #344 dependency change in this repository. Do not use dynamic import as a size control. ### D6 — Security opt-in @@ -983,8 +911,9 @@ The design uses salted SHA-256 membership operators and binary structure. It does not use a separate obfuscated payload mode. React Native does not pre-hash customer context. -Do not claim support until G6, G11, and G12 are complete. -Reject unsupported operators as `GENERAL`. +Do not claim complete support until G12 is complete. +Let the upstream parser drop unsupported flags. +Return `FLAG_NOT_FOUND` for a dropped flag. Use canonical test vectors. A public salt stops reusable precomputation. @@ -993,10 +922,11 @@ Document all visible UFC data. ### D8 — Targeting key -**Status:** Open. +**Decision:** Missing and empty keys are different. -Decide whether missing and empty keys are different. -Then, update the types, error codes, adapter, and documentation. +Preserve `undefined` in the internal rules context. +Treat an empty string as a real key. +Return `TARGETING_KEY_MISSING` only when the selected shard requires a key. ### D9 — `id` and `targetingKey` @@ -1033,46 +963,57 @@ Alternative: ### Q1 — Published flagging-core version - [ ] Identify the version that contains rules wire parsing. -- [ ] Confirm the final rules field name. -- [ ] Confirm the final wire version. -- [ ] Confirm that `configurationFromString` populates the rules branch. +- [x] Confirm the rules field name: `rules`. +- [x] Confirm the wire version: `1`. +- [x] Confirm that PR #344 `configurationFromString` populates the rules branch. ### Q2 — Protobuf implementation -- [ ] Publish the `.proto` schema. -- [ ] Define the protobuf runtime. +- [x] Add the `.proto` schema to the upstream repository. +- [x] Select Protobuf-ES as the runtime. - [ ] Confirm Hermes compatibility. - [ ] Confirm JSC compatibility. -- [ ] Define unknown-enum behavior. -- [ ] Add SHA operators and salt fields to the schema. +- [x] Define per-flag feature-level and unknown-field behavior. +- [x] Add SHA operators and salt fields to the schema. +- [ ] Confirm the salt-length policy. +- [ ] Define configuration-size limits. +- [ ] Publish cross-SDK hash vectors. ### Q3 — Tracking metadata -- [ ] Decide how rules evaluation returns `extraLogging`. -- [ ] Confirm whether native telemetry needs the original UFC numeric type. +- [x] Treat `extraLogging` as deprecated upstream. +- [ ] Confirm that the current mobile exposure event is sufficient for rules evaluations. +- [ ] Confirm whether mobile telemetry needs the original integer or numeric type. +- [ ] Confirm whether default and error evaluations need a new native API. ## 10. Review history The plan had six review rounds on 2026-07-22 and 2026-07-23. -The reviews checked local version 2.0.1, PR #336, native tracking, and OpenFeature behavior. +The plan was updated on 2026-07-27 after review of PR #343, PR #344, and PR #336. The reviews produced these main corrections: -- The rules engine already exists in version 2.0.1. +- The rules engine already exists in the published package. +- Version 2.0.2 removes an unnecessary dependency but does not add rules wire parsing. +- PR #344 defines the expected `rules` protobuf contract. +- PR #336 proves browser provider integration with that contract. - React Native must call the rules-only evaluator. - Path selection must occur for each resolution. - Native tracking must cross the bridge for every successful assignment. +- The current native bridge is sufficient unless the mobile exposure contract changes. +- `extraLogging` is deprecated and is not an upstream blocker. - The engine already adds bundle size today. -- Protobuf and synchronous SHA-256 add future bundle size. -- Rules validation must protect against malformed input and ReDoS. +- PR #344 adds Protobuf-ES and synchronous SHA-256. +- PR #344 performs structural validation. +- Regular-expression safety still requires a decision. - A missing targeting key differs from an empty string in the evaluator. - The evaluator flag lookup is not safe for prototype names. - The public parsed configuration type is not fully opaque. - OpenFeature types currently resolve through hoisting. - Custom `id` can conflict with `targetingKey`. -- Unsupported operators currently cause a silent `DEFAULT`. -- Operator validation should belong to flagging-core. -- Unsupported rules must not remove a valid precomputed branch. +- PR #344 drops unsupported flags and keeps valid flags. +- Unsupported flags must return `FLAG_NOT_FOUND`, not a silent `DEFAULT`. +- A malformed branch must not remove a valid sibling branch. - The salted-hash protocol needs canonical cross-SDK test vectors. - Salted SHA-256 does not make low-entropy values confidential. - Platform opt-in does not apply to customer-supplied wires. From 7b00adcb51c82074637e0b6b6f8109a3490355dd Mon Sep 17 00:00:00 2001 From: Blake Date: Tue, 28 Jul 2026 12:51:11 -0400 Subject: [PATCH 8/8] docs: update dynamic offline plans for upstream progress --- dynamic_offline_pr_stack.plan.md | 23 +++++++-- dynamic_offline_simplified.plan.md | 79 ++++++++++++++++++------------ 2 files changed, 67 insertions(+), 35 deletions(-) diff --git a/dynamic_offline_pr_stack.plan.md b/dynamic_offline_pr_stack.plan.md index c12d63299..d9815da7a 100644 --- a/dynamic_offline_pr_stack.plan.md +++ b/dynamic_offline_pr_stack.plan.md @@ -19,8 +19,11 @@ Keep all three pull requests in draft state. ## Temporary upstream code Published flagging-core version 2.0.2 does not contain the new rules wire contract. -Upstream PR #344 adds the protobuf rules parser, SHA-256 evaluation, validation, and React Native compatibility. +Upstream PR #344 adds the generated Protobuf-ES rules parser, SHA-256 evaluation, validation, safe flag lookup, and React Native compatibility. +It adds `@bufbuild/protobuf` as a runtime dependency. +Its packed-package smoke test uses the Metro export conditions from this repository. Upstream PR #336 uses that parser in the browser `CoreProvider`. +PR #336 also uses the safe upstream lookup for precomputed flags. Put a `TODO` immediately before each temporary implementation. The `TODO` must identify the upstream replacement. @@ -28,7 +31,8 @@ Do not hide temporary behavior in a general helper. Tests can use a fake rules engine. Production code must use one internal engine adapter. -Remove temporary JSON rules-wire parsing and duplicate rules validation after the upstream package is published. +Remove temporary JSON rules-wire parsing, duplicate rules validation, and local lookup guards after the upstream package is published. +Do not add a local protobuf parser. Do not wait for upstream `extraLogging`. The field is deprecated. Use an empty object only where the current Android bridge requires it. @@ -42,16 +46,22 @@ Add the internal boundary for the rules engine. - Use `FlagsConfiguration.rules.response`. - Remove the temporary `rulesBased` and JSON compatibility shapes. - Remove duplicate structural validation after the dependency bump. +- Remove the temporary own-property guard after the dependency bump. - Add internal rules configuration types. - Add a rules-engine adapter. - Convert SDK contexts to engine contexts. - Normalize engine results. - Use the upstream protobuf rules object. - Use upstream parser validation. -- Add an own-property guard for flag lookup until upstream fixes it. +- Derive the rules response type from `FlagsConfiguration['rules']`. +- Do not export generated UFC message types. +- Keep OpenFeature types out of React Native core. +- Use compatible internal context and logger types. +- Verify that the pinned evaluator returns `FLAG_NOT_FOUND` for absent reserved-name keys. - Keep regular-expression safety as an explicit open item. - Add adapter contract tests. - Add a protobuf wire contract test. +- Add reserved-name flag-key contract tests. - Confirm that rules serialization throws. - Add fake-engine test helpers. - Keep current provider behavior unchanged. @@ -79,6 +89,8 @@ Add dynamic evaluation to `FlagsClient`. - Track each successful rules assignment through the current native bridge. - Let native code apply `doLog` to exposure events. - Do not require split serial ID or evaluation timestamp in the mobile exposure payload unless the mobile contract changes. +- Record that integer and numeric variations both use the OpenFeature type `number`. +- Confirm whether mobile telemetry must preserve the original integer or numeric type. - Keep online and precomputed behavior unchanged. - Add rules-only and mixed-configuration tests. - Use the fake engine for path-selection tests. @@ -103,7 +115,10 @@ Expose dynamic evaluation through the existing offline provider. - Update the provider documentation. - Update both example applications. - Add Hermes and JSC checks where the repository supports them. -- Record the upstream protobuf bundle-size measurement. +- Test the packed dependency with the repository Metro export conditions. +- Record the 6,229-byte minified and 2,070-byte gzipped browser bundle increase. +- Record the 1,106-byte minified and 459-byte gzipped React Native compatibility cost. +- Measure the packed dependency in this repository. The main review question is: diff --git a/dynamic_offline_simplified.plan.md b/dynamic_offline_simplified.plan.md index c66d72ee1..60f57b816 100644 --- a/dynamic_offline_simplified.plan.md +++ b/dynamic_offline_simplified.plan.md @@ -4,7 +4,7 @@ This document uses Simplified Technical English. Technical names and API names do not change. **Jira:** FFL-2837 -**Base branch:** `blake.thomas/FFL-2666` +**Base branch:** `develop` **Work branch:** `blake.thomas/FFL-2837` **Upstream references:** DataDog/openfeature-js-client PRs #343, #344, and #336 @@ -77,17 +77,21 @@ It adds these features: - A `rules` field in `FlagsConfiguration` - A protobuf and base64 rules response - The checked-in UFC protobuf schema +- Generated Protobuf-ES message types from the canonical UFC schema - Protobuf-ES decoding - Independent parsing of the precomputed and rules branches - Per-flag validation and feature-level rejection - `ONE_OF_SHA256` and `NOT_ONE_OF_SHA256` - A synchronous JavaScript SHA-256 implementation - An internal UTF-8 implementation +- Own-property lookup for legacy and protobuf rules flag maps - A React Native Metro smoke test The parser keeps a valid branch when the other branch is malformed. The parser drops an unsupported or invalid rules flag. It does not invalidate the complete rules branch. +The schema is copied from the canonical `dd-source` UFC schema. +The generated message types are compiled into the package output. The published package will expose `configuration.rules.response`. Do not use the old planned name `rulesBased`. @@ -97,6 +101,8 @@ Do not use the old planned name `rulesBased`. PR #336 is stacked on PR #344. It adds the browser `CoreProvider` and a combined core `evaluate` function. It proves that `configurationFromString` returns a rules object that the evaluator can use. +Its head did not change during the 2026-07-28 review. +Its updated PR #344 base supplies the new decoder and lookup fixes. Use the browser `CoreProvider` as an integration reference. @@ -138,6 +144,9 @@ The metadata can contain these fields: - `__dd_do_log` - `__dd_eval_timestamp_ms` +The evaluator reports both protobuf integer and numeric variations as the OpenFeature type `number`. +It does not preserve that distinction in `variationType`. + The split serial ID and evaluation timestamp are server-side tracing metadata. The current Android and iOS mobile exposure APIs do not accept these fields. The native mobile SDK creates the exposure timestamp. @@ -168,8 +177,14 @@ Do not serialize a decoded rules configuration. PR #344 makes `configurationToString` throw when rules are present. PR #344 adds Protobuf-ES to the bundle. -Its browser measurement reports an increase of 6,229 bytes minified and 2,070 bytes compressed. +`@bufbuild/protobuf` is a runtime dependency. +The schema generators are development dependencies. +Its browser measurement reports an increase of 6,229 bytes minified and 2,070 bytes gzipped. +These increases are 9.6 percent and 10.4 percent. +The React Native compatibility code accounts for 1,106 minified bytes and 459 gzipped bytes. +The upstream decision accepts this cost to reduce decoder maintenance risk. Its React Native smoke test bundles Android and iOS with React Native 0.76.9. +The test uses the packed flagging-core package and the export-condition order from this repository. The test runs the Android bundle under Node without `TextEncoder`, `TextDecoder`, or `BigInt`. It does not run the bundle in Hermes or JSC. @@ -189,14 +204,16 @@ After publication, bump `@datadog/flagging-core` in `packages/core/package.json` ### G2 — Protobuf rules response -**Status:** Mostly implemented in PR #344. +**Status:** Implemented in PR #344. Publication and runtime verification are pending. -PR #344 includes the schema and the Protobuf-ES parser. +PR #344 includes the canonical schema, generated message types, and the Protobuf-ES parser. React Native must use the opaque parser. Do not parse protobuf in this repository. -The raw `.proto` file is not part of the current npm package file list. -Confirm whether repository publication is sufficient. +The npm package contains compiled CommonJS, ESM, and declaration outputs. +It does not contain the raw `.proto` source. +The runtime does not need the raw source. +Keep the schema and generation instructions in the upstream repository for review and regeneration. Run the packed package in Hermes and JSC. ### G3 — Root exports @@ -241,6 +258,7 @@ If they require split serial ID, evaluator timestamp, or error evaluations, add PR #343 removes an unnecessary dependency. PR #344 measures the browser protobuf cost. PR #344 also adds a React Native Metro smoke test. +The upstream PR accepts the measured bundle increase. Measure the packed dependency in this repository. Run the rules flow in Hermes and JSC. @@ -301,32 +319,25 @@ Relax the internal context type and error union. ### G9 — Prototype-unsafe flag lookup -**Status:** Open upstream bug. - -Both upstream rules evaluators use an unsafe object lookup. -The combined evaluator in PR #336 also uses an unsafe precomputed lookup. -A missing reserved key can resolve through `Object.prototype`. - -Examples include: +**Status:** Implemented in PR #344. Publication is pending. -- `toString` -- `__proto__` -- `constructor` +PR #344 adds a shared own-property helper. +The legacy rules evaluator and the protobuf evaluator use it. +PR #336 uses it for the combined evaluator precomputed path. -The protobuf evaluator can return `TYPE_MISMATCH` instead of `FLAG_NOT_FOUND`. +Reserved names such as `toString`, `__proto__`, and `constructor` now return `FLAG_NOT_FOUND` when they are not real flag keys. +The React Native precomputed cache also uses a `Map`. -Use an own-property check before evaluation. -Request the upstream fix in PR #344 and PR #336. -Keep a React Native guard until the released dependency contains the fix. -The React Native precomputed cache already uses a `Map`. +Pin the released dependency that contains this fix. +Keep reserved-name contract tests in React Native. +Do not add a duplicate local guard after that dependency is available. ### G10 — OpenFeature type dependency **Status:** Decision required. -`packages/core` has no OpenFeature dependency. The flagging-core declaration files import `@openfeature/core`. -Flagging-core lists that package only as a development dependency. +Flagging-core lists that package as a development dependency, not as a runtime or peer dependency. Local hoisting hides this problem. Select one solution: @@ -383,7 +394,7 @@ Publish cross-SDK vectors. - [ ] Publish the SHA operators and synchronous SHA-256 implementation. - [ ] Confirm the remaining salt and size-limit rules. - [ ] Confirm the current mobile exposure contract. -- [ ] Fix prototype-unsafe lookup upstream or keep the local guard. +- [ ] Pin the flagging-core release that contains the own-property lookup fix. - [ ] Select a regular-expression safety policy. - [ ] Bump `@datadog/flagging-core` in `packages/core`. - [ ] Update `yarn.lock`. @@ -436,7 +447,8 @@ Return `GENERAL` only when the parser returns no usable branch. Do not add a second structural rules validator. Trust the parser to drop unsupported or malformed rules flags. -Keep the own-property guard from G9 until upstream contains the fix. +Trust the released evaluator to perform own-property lookup. +Keep the reserved-name contract tests from G9. Apply the size policy from G12 after the policy is defined. ### Step 4 — Reconcile the context @@ -495,8 +507,7 @@ Map all evaluator results to `FlagDetails`. The evaluator already returns `FLAG_NOT_FOUND`. Do not create this error again in React Native. -Add an own-property check for the flag key. -Return `FLAG_NOT_FOUND` for an absent reserved-name key. +Verify that the pinned upstream evaluator returns `FLAG_NOT_FOUND` for an absent reserved-name key. Convert a successful rules result to an internal `TrackableAssignment`. Track every successful assignment through the native bridge. @@ -860,8 +871,9 @@ Complete D11 before you transfer OpenFeature types into core. ### R14 — Prototype lookup -Reserved-name keys can return the wrong result. -Use an own-property check or an upstream fix. +PR #344 fixes reserved-name lookup. +Pin the release that contains the fix. +Keep React Native contract tests to prevent a dependency regression. ### R15 — Subject identifier @@ -982,14 +994,16 @@ Alternative: ### Q3 — Tracking metadata - [x] Treat `extraLogging` as deprecated upstream. +- [x] Confirm that flagging-core reports integer and numeric variations as `number`. - [ ] Confirm that the current mobile exposure event is sufficient for rules evaluations. -- [ ] Confirm whether mobile telemetry needs the original integer or numeric type. +- [ ] Confirm whether mobile telemetry must distinguish the original integer and numeric types. - [ ] Confirm whether default and error evaluations need a new native API. ## 10. Review history The plan had six review rounds on 2026-07-22 and 2026-07-23. The plan was updated on 2026-07-27 after review of PR #343, PR #344, and PR #336. +The plan was updated again on 2026-07-28 after PR #344 added generated Protobuf-ES support, package smoke tests, and safe flag lookup. The reviews produced these main corrections: @@ -1004,10 +1018,13 @@ The reviews produced these main corrections: - `extraLogging` is deprecated and is not an upstream blocker. - The engine already adds bundle size today. - PR #344 adds Protobuf-ES and synchronous SHA-256. +- PR #344 accepts a measured 6,229-byte minified and 2,070-byte gzipped browser bundle increase. +- PR #344 tests a packed package with the React Native Metro export conditions. - PR #344 performs structural validation. - Regular-expression safety still requires a decision. - A missing targeting key differs from an empty string in the evaluator. -- The evaluator flag lookup is not safe for prototype names. +- PR #344 fixes prototype-name lookup in both rules evaluators. +- PR #336 uses the same safe lookup for its precomputed evaluator. - The public parsed configuration type is not fully opaque. - OpenFeature types currently resolve through hoisting. - Custom `id` can conflict with `targetingKey`.