Skip to content

FFL-3568: Share precomputed fixtures and lossless numeric policy - #4101

Draft
leoromanovsky wants to merge 51 commits into
aarsilv/ffl-3565/numeric-type-mismatchfrom
leo.romanovsky/ffl-3568/precompute-fixture-fanout
Draft

leoromanovsky wants to merge 51 commits into
aarsilv/ffl-3565/numeric-type-mismatchfrom
leo.romanovsky/ffl-3568/precompute-fixture-fanout

Conversation

@leoromanovsky

@leoromanovsky leoromanovsky commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

What does this PR do?

Make native flag evaluations and emitted events conform to the merged shared contract. The 63-case snapshot pins fixture main at 9afc3f75c3fef9643a9c1b5e2b1e5a9adac48177 and runs through the public client before and after cache restoration. Ref: FFL-3568.

Motivation

Consumers need one tested rule for numeric reads, unsupported variation types, obfuscated flag keys, and event aggregation. A typed read must not silently lose integer precision or log a successful exposure for a rejected conversion.

Additional Notes

Stacked on #4099, retaining its converter structure and bounded decimal parsing. The additional checks align it with the merged contract: fractional Double reads round, whole values must be exactly representable, and overflow/nonzero underflow return TYPE_MISMATCH. For example, 1.20000000000000001 returns 1.2; 1e300 is rejected as an inexact whole-number Double read. Malformed numeric text remains a parse error.

Unsupported variation types are omitted with a once-only internal user warning, leaving valid siblings available. Missing extraLogging defaults are compatible with shared responses. Obfuscated lookups and real emitted records retain plaintext flag identity. The runner reports an actionable fixture error for an applicable null targeting key; the native context API requires a string.

The shared importer preserves provenance and response tokens. A weekly workflow prepares draft fixture updates. Obsolete converter detekt entries were removed; entries required by the merged code remain.

Validation: 1,007 feature unit tests and 259 integration tests pass, with 3 integration platform exclusions. Affected-module formatting and detekt pass. All snapshot integrity/source checks pass. Hosted comparison is tracked in the native preview rerun; these are unit-level checks, not deployed-backend validation.

Review checklist (to be filled by reviewers)

  • Feature or bugfix MUST have appropriate tests (unit, integration, e2e)
  • Make sure you discussed the feature or bugfix with the maintaining team in an Issue
  • Make sure each commit and the PR mention the Issue number (cf the CONTRIBUTING doc)

Add FlagsConfiguration.Builder.flagSelection(tags, keyPatterns). The
selection is canonicalized and validated like Compute and ffe-service,
sent as flag_selection on the precompute POST, and stored with the
persisted state so a cache fetched with another selection is ignored.
Report DD-Flag-Selection-Ignored, and treat a valid empty configuration
as loaded so a later failed fetch reports Stale.

Ref: FFL-3459
Persist the flag selection through the generated FlagsState model and
re-apply selection filtering, empty-config handling and the ignored
header on top of develop's obfuscation changes.

Ref: FFL-3459
The downloader reports DD-Flag-Selection-Ignored only for requests that
carried flag_selection, and passes the value through verbatim.

Ref: FFL-3459
Builder.flagSelection takes List<String>? for tags and key patterns. A
null list is treated as absent, matching the browser SDK; null elements
inside a list remain malformed_selection.

Ref: FFL-3459
State that limits count values after dedupe, that flags outside the
selection return the default with FLAG_NOT_FOUND, and when the server
returns the full configuration instead.

Ref: FFL-3459
Add hand-written canonicalizer cases for the selection vectors with sdk
expectations that the existing tests did not cover.

Ref: FFL-3459
Treat an empty DD-Flag-Selection-Ignored value as absent, as the browser
SDK does, so it no longer logs a server ignore with an empty reason.
Clarify in the flagSelection KDoc that the byte limit applies after
trimming, that unpaired UTF-16 surrogates are malformed, and that
FLAG_NOT_FOUND applies when the server applies the selection.

Ref: FFL-3459
Verify that FlagsClient passes the configured selection to the
repository, so a persisted state saved under that selection is served.
Verify that a persisted state saved without a selection is discarded once
a selection is configured. Extend the trim test to every Unicode
White_Space character.

Ref: FFL-3459
Dedupe selection values before sorting, compute each UTF-8 sort key once,
and skip sorting a list that is already over the value limit. Pass the
response mapper by reference. Drop positional null arguments in tests
that only restated the default obfuscation value.

Ref: FFL-3459
Send the canonical selection through the generated request model that develop introduced.
The reason is reported before the body is parsed, so it is logged even when the configuration is not installed.
List all 38 cases that have an sdk expectation in the shared vectors, in file order, so each one maps to one test row.
A Java caller can pass a raw list, so the canonicalizer checks each element's type. A non-string element is malformed_selection, as in the browser SDK, instead of a ClassCastException from Flags.enable.
SDK versions without flag selection read flags-state-<instance> with any version and ignore the flagSelection field, so a rollback served a selected subset as the full configuration. Selected state now uses flags-selection-state-<instance>, and creating an instance removes the other key so one state remains per instance. Unselected state stays where older SDKs read it.
The header value reaches USER logs and telemetry, so keep the five reasons the server sends and report any other non-empty value as unknown. The selection is still treated as ignored and the full configuration is still installed.
hasFlags() and hasLoadedFlagsForContext() report whether a configuration
is installed, including a valid one with zero flags. Name them
hasConfiguration() and hasConfigurationForContext() so call sites read
as intended.
Log the DD-Flag-Selection-Ignored reason and send its telemetry only after
the response maps to a configuration and passes the obfuscation filter,
just before it is installed. A body that fails to parse, or an encoded
configuration this source cannot use, no longer reports a reason for a
configuration that was never loaded.
mark-caii and others added 16 commits October 29, 2026 17:03
Resolve the flag selection and experimentation opt-out conflicts:

- Each opt-out mode gets a selection variant of its cache key
  (flags-selection-state, flags-selection-immediate-v1-state,
  flags-selection-future-v1-state). SDK versions without flag selection
  read only the unselected key of their mode, so they never load selected
  state as a full configuration, and creating an instance removes the
  sibling key so one persisted state remains per instance.
- The per-client request factory used for opt-out clients now carries
  the flag selection, so a request sends both flag_selection and
  evaluation_options when both apply.
- Keep the hasConfiguration() names, read the ignored reason from the
  accepted response, and map the response body.
The dropped and ignored selection telemetry events now use onlyOnce, so
frequent fetches cannot flood telemetry. The USER error log is unchanged
and still reports every ignored response.
With the experimentation opt-out, a newer context can supersede an
accepted response, which is then not installed. Log the ignored reason
after installFlags confirms the install, so the message never claims a
configuration that was not loaded.
Resolve the DefaultFlagsRepository constructor conflict: keep develop's
change that makes instanceName and dataStore plain constructor
parameters, and treat the branch's flagSelection the same way, since it
is only read while building the persistence manager.
Say that the flag selection is what every FlagsClient shares, and use a
function reference for the empty ignored-reason header check.
…pendency-of-test-fixtures-on-tools-unit

RUM-19577: Break dependency of test fixtures on `:tools:unit` module
Run the canonical JSON corpus through the client and persistent cache. Enforce numeric getter types, preserve legacy number reads, and automate signed fixture refreshes.
FFL-3459: Support flagSelection on Flags precompute requests
@datadog-datadog-us1-prod

datadog-datadog-us1-prod Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Pipelines  Tests

🎉 All green!

🧪 All tests passed
❄️ No new flaky tests detected

🎯 Code Coverage (details)
• Patch Coverage: 95.61%
• Overall Coverage: 72.18% (+0.13%)

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

@aarsilv

aarsilv commented Nov 4, 2026

Copy link
Copy Markdown

🤖 Comment from Claude, working with Aaron:

This PR and #4099 both change FlagValueConverter. Could you stack this PR on #4099 and keep its converter? This PR would then keep the fixture runner, the sync workflow, the extraLogging default and the handling of unsupported variation types. If you prefer your rules for the two differences below, we can change #4099 first.

#4099 now checks whole values in Double reads as well as Int reads. We ran the shared fixtures from DataDog/ffe-system-test-data#65 against it with this PR's runner, merged with develop. The local test copy adds "extraLogging": {} to each fixture assignment. Of 95 runs, 88 pass, 3 are skipped and 4 fail. The 4 failures expect FLAG_NOT_FOUND for an unsupported variationType, and this PR omits those flags.

Two differences to settle:

Could you also update the description's statement that #4099 "still enforces the previous strict-type contract"? #4099 already has lossless Int reads across all numeric types.

@aarsilv

aarsilv commented Nov 4, 2026

Copy link
Copy Markdown

🤖 Comment from Claude, working with Aaron:

P3 follow-ups after rebasing on #4099:

  • README numeric text. The new section says that inexact conversions return TYPE_MISMATCH. With FFL-3565: Accept every numeric type in Int and Double reads #4099, a Double read of 1.20000000000000001 returns the nearest Double. For NaN, resolve() reports PARSE_ERROR. Could this section link to the existing numeric paragraph in the README?
  • detekt entries. Most new entries support this PR's converter. Examples include BigDecimal.intValueExact(), Result.success(kotlin.Int), and Double.isFinite(). After rebasing, could you remove entries that the resulting code no longer uses? Keep entries for calls that remain, including EnumEntries.none in FlagsJson.
  • Unsupported types. PrecomputeMapper drops flags with an unsupported variationType without logging the cause. Once the client is ready, a lookup reports FLAG_NOT_FOUND, as the fixtures expect. Could the mapper log a warning once through the internal logger to explain the cause?
  • targetingKey: null. The fixture schema permits context.targetingKey to be null. At line 63, CanonicalPrecomputedTest calls context["targetingKey"].asString, which throws for JSON null. None of the 57 current fixtures uses a null targeting key. Could the runner handle null, or could future fixtures with null exclude Android through _skip?

Preserve original numeric precision for typed reads and cache restoration.

Ref: FFL-3568
Merge the dependency branch, pin shared fixtures to main at 9afc3f7, and address runner and numeric review feedback.

Ref: FFL-3568
@leoromanovsky
leoromanovsky changed the base branch from develop to aarsilv/ffl-3565/numeric-type-mismatch November 4, 2026 18:51

Copy link
Copy Markdown
Contributor Author

@aarsilv Follow-ups addressed in a45b99c342 and the numeric documentation follow-up:

  • Stacked on FFL-3565: Accept every numeric type in Int and Double reads #4099 with a merge commit and changed the PR base. Retained its converter structure and 64-character decimal parsing bound. The merged fixture contract requires nearest-Double fractional reads, exact original whole-number reads, and rejection of nonzero underflow. The remaining delta enforces those rules, including rejecting 1e300 as an inexact whole value. The PR description and README no longer describe FFL-3565: Accept every numeric type in Int and Double reads #4099 as strict-type or promise the old printed-form exception.
  • Removed obsolete converter detekt entries; retained the calls the merged implementation uses, including the unsupported-type detection.
  • Added a once-only internal USER warning for omitted unsupported variation types, plus coverage that valid sibling flags survive and the warning requests onlyOnce.
  • Defined null-targeting-key applicability explicitly: Android requires a string, so an applicable null case fails with an instruction to declare the exclusion upstream instead of throwing from asString.

The snapshot pins fixture main at 9afc3f75. Local validation passes: 1,007 feature tests, 259 integration tests with 3 platform exclusions, formatting, and detekt.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants