feat(openfeature): add semantic version operators for feature flag evaluation - #12182
feat(openfeature): add semantic version operators for feature flag evaluation#12182greghuels wants to merge 5 commits into
Conversation
Port the SemVer condition operators (SEMVER_EQ, SEMVER_NEQ, SEMVER_LT, SEMVER_LTE, SEMVER_GT, SEMVER_GTE) from dd-trace-go PR #5128 to the Java SDK. This includes: - ParsedSemver: a Rust-compatible SemVer parser and comparator that owns its own parsing logic for cross-SDK consistency. Core components use unsigned 64-bit semantics; numeric prerelease identifiers support arbitrary length; build metadata is validated but ignored for precedence. - ConditionOperator: 6 new enum values for the SEMVER_* operators. - ConditionConfiguration: transient semverComparand field to cache the parsed comparand during config validation. - ServerConfiguration: transient invalidFlags map so the evaluator can return PARSE_ERROR for flags with invalid semver comparands. - DDEvaluator: evaluateSemverCondition method and switch cases for all 6 operators; invalid-flag check returning PARSE_ERROR. - UniversalFlagConfigParser: validateAndCacheSemverComparands during parsing, with InvalidSemverComparandException for non-string or unparseable values. - Update ffe-system-test-data submodule to main (ea8b5cc) which includes the semver comparison and validation fixtures from PRs #20 and #22. - Unit tests: ParsedSemverTest (30 tests ported from Go semver_test.go), DDEvaluatorTest semver condition tests (28 parameterized + 2 edge cases), and canonical fixture tests (22 cases from the two system test PRs).
|
🎯 Code Coverage (details) 🔗 Commit SHA: 21e2451 | Docs | Datadog PR Page | Give us feedback! |
🟢 Java Benchmark SLOs — All performance SLOs passed
PR vs. master results
Commit: Load and DaCapo benchmarks can be triggered manually in the GitLab pipeline. Results will appear in the Benchmarking Platform UI after completion. |
- DDEvaluatorTest: Add LenientFlagMapAdapter to handle flags with unknown
operators (NOT_A_REAL_OPERATOR) without throwing JsonDataException.
Track all dropped flags in config.invalidFlags so the evaluator returns
PARSE_ERROR instead of FLAG_NOT_FOUND.
- DDEvaluator: Multiple correctness fixes required by the updated fixture suite:
- Return PARSE_ERROR (not TYPE_MISMATCH) for numeric condition parse errors
- Throw InvalidFlagConfigException (→ PARSE_ERROR) for ONE_OF with non-array
value, IS_NULL with non-boolean value, invalid shard bounds (totalShards ≤ 0
or > unsigned-32-bit max, negative/null shard ranges), missing split shards
- Return DEFAULT reason for date-filtered allocations instead of STATIC
- Translate POSIX character classes ([:alnum:] etc.) to Java regex equivalents
- Check config.invalidFlags.containsKey() for any invalid flag (not only
invalid_semver_comparand)
- Shard: Change totalShards from int to long so Moshi stores 4294967296 as
the correct value (4294967296L) rather than silently clamping to Integer.MAX_VALUE,
enabling the out-of-unsigned-32-bit-range validation to fire correctly.
- UniversalFlagConfigParser: Track all dropped flags in INVALID_FLAGS_HOLDER
(not only invalid_semver_comparand), so evaluator returns PARSE_ERROR for any
malformed flag.
- ParsedSemverTest: Add equals/hashCode tests and uppercase identifier tests to
push branch coverage above the 90% JaCoCo threshold.
- UniversalFlagConfigParserTest: New test class covering semver validation,
unknown operators, null allocations/rules/conditions, and cross-flag isolation.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
|
Hi! 👋 Thanks for your pull request! 🎉 To help us review it, please make sure to:
If you need help, please check our contributing guidelines. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a9aba09100
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if (config.invalidFlags != null && config.invalidFlags.containsKey(key)) { | ||
| return error( | ||
| defaultValue, ErrorCode.PARSE_ERROR, "invalid configuration for flag " + key); |
There was a problem hiding this comment.
Limit missing-flag parse errors to semver validation
When a malformed non-semver flag is dropped by UniversalFlagConfigParser.FlagMapAdapter, it is now recorded in invalidFlags as parse_error, and this branch turns any subsequent lookup of that missing key into PARSE_ERROR. That changes the existing behavior for ordinary malformed/unknown flags from FLAG_NOT_FOUND to a parse error; only flags dropped specifically because their semver comparand is invalid need this special evaluation error.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
This is intentional. The updated ffe-system-test-data fixture includes flags with various malformations (invalid operators, non-list allocations, etc.), not just invalid semver comparands. If a flag was present in the config but dropped by the parser due to any parse error, returning FLAG_NOT_FOUND would be misleading — the flag exists but is malformed. PARSE_ERROR correctly communicates that the flag's configuration is invalid, which is what the evaluation test cases in the fixture expect.
There was a problem hiding this comment.
Only pull in updates from the git submodule that include your semver fixtures; I'll address the flag invalidate ones in a follow up.
buildLoggedAllocations now checks instanceof Collection before iterating allocations, preventing MissingPropertyException when allocations is a non-list type (e.g. the intentionally malformed "malformed-allocations-flag" in ffe-system-test-data).
Change getShard to accept long totalShards instead of int, preventing negative modulo when totalShards is in the upper half of the unsigned 32-bit range (0x80000000–0xFFFFFFFF). The (int) cast was converting valid unsigned values to negative ints, causing every targeting key to hash to shard 0.
There was a problem hiding this comment.
Shard totals above 2^31 are accepted but narrowed during hashing, silently selecting different variations than Go. The new SemVer preprocessing also dereferences null nested list elements, so one malformed flag can reject an otherwise valid remote-config update.
📊 Validated against 9 scenarios · Open Bits AI session
🤖 Datadog Autotest · Commit a9aba09 · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest
| throw new InvalidFlagConfigException( | ||
| "Invalid shard totalShards (must be in unsigned 32-bit range): " + shard.totalShards); | ||
| } | ||
| final int assignedShard = getShard(shard.salt, targetingKey, (int) shard.totalShards); |
There was a problem hiding this comment.
Keep unsigned shard arithmetic wide
Users with shard totals above Integer.MAX_VALUE can silently receive incorrect feature-flag variations and inconsistent cross-SDK rollout assignments.
Assertion details
- Input: A shard with totalShards=3,000,000,000, salt="salt", targeting key "subject", and range [1,490,462,878, 1,490,462,879).
- Expected:
Every total accepted by the unsigned-32-bit validation should be hashed using wide arithmetic and select the same split across SDKs. - Actual:
The allowed 3,000,000,000 total is cast to a signed int before modulo. For salt "salt" and targeting key "subject", the correct shard is 1,490,462,878, but Java calculates 195,495,582 and returns DEFAULT. The complete fix must retain long arithmetic through assignedShard, getShard, and modulo; supporting the full uint32 range also requires widening shard-range bounds, so no isolated suggestion is safe.
Was this helpful? React 👍 or 👎
🤖 Datadog Autotest · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest
| } | ||
| for (int allocIdx = 0; allocIdx < flag.allocations.size(); allocIdx++) { | ||
| final Allocation allocation = flag.allocations.get(allocIdx); | ||
| if (allocation.rules == null) { |
There was a problem hiding this comment.
Isolate null nested config elements
One malformed flag can prevent an entire remote-config update from applying, leaving every flag on stale configuration.
Assertion details
- Input: A flags map containing one valid flag and one flag whose allocations, rules, or conditions array contains JSON null.
- Expected:
Malformed nested elements should be tracked against their containing flag without preventing valid sibling flags from being parsed. - Actual:
The preprocessing loop dereferences null allocation, rule, and condition elements. All three tested JSON shapes throw NullPointerException from deserialize(), rejecting the complete configuration and its valid sibling flag. The fix needs separate validation at each nesting level that raises a caught per-flag parse error, so a single line-bounded suggestion is incomplete.
Was this helpful? React 👍 or 👎
🤖 Datadog Autotest · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest
There was a problem hiding this comment.
We'll address this in a follow-up PR; more ffe fixture updates coming.
Wrap method parameters across two lines to satisfy spotlessJavaCheck.
| package datadog.trace.api.featureflag.ufc.v1; | ||
|
|
||
| /** | ||
| * ParsedSemver is the language-neutral representation of the Rust/Eppo SemVer subset used by FFE. |
| } | ||
| for (int allocIdx = 0; allocIdx < flag.allocations.size(); allocIdx++) { | ||
| final Allocation allocation = flag.allocations.get(allocIdx); | ||
| if (allocation.rules == null) { |
There was a problem hiding this comment.
We'll address this in a follow-up PR; more ffe fixture updates coming.
| if (config.invalidFlags != null && config.invalidFlags.containsKey(key)) { | ||
| return error( | ||
| defaultValue, ErrorCode.PARSE_ERROR, "invalid configuration for flag " + key); |
There was a problem hiding this comment.
Only pull in updates from the git submodule that include your semver fixtures; I'll address the flag invalidate ones in a follow up.
What Does This Do
Ports SemVer condition operators (
SEMVER_EQ,SEMVER_NEQ,SEMVER_LT,SEMVER_LTE,SEMVER_GT,SEMVER_GTE) from dd-trace-go#5128 to the Java SDK.ParsedSemver— Rust-compatible SemVer parser and comparator (unsigned 64-bit core components, arbitrary-length numeric prerelease identifiers, build metadata validated but ignored for precedence).ConditionOperator— 6 new enum values.ConditionConfiguration— transientsemverComparandfield cached during config validation.ServerConfiguration— transientinvalidFlagsmap so the evaluator returnsPARSE_ERRORfor flags with invalid semver comparands.DDEvaluator—evaluateSemverConditionmethod and switch cases for all 6 operators; invalid-flag check returningPARSE_ERROR.UniversalFlagConfigParser— validates and caches semver comparands during parsing; invalid comparands cause the flag to be dropped and tracked.ffe-system-test-datasubmodule — updated tomain(ea8b5cc) which includes the semver comparison and validation fixtures from ffe-system-test-data#20 and ffe-system-test-data#22.Motivation
Cross-SDK feature flag parity. The Go SDK added SemVer operators in dd-trace-go#5128; this ports the same functionality to Java so FFE targeting rules using
SEMVER_*operators evaluate identically across SDKs.Additional Notes
ParsedSemverTest(30 tests ported from Gosemver_test.go),DDEvaluatorTestsemver condition tests (28 parameterized + 2 edge cases).testEvaluateCanonicalFixtureparameterized test and theOpenFeatureProviderSmokeTest.Contributor Checklist
type:and (comp:orinst:) labels in addition to any other useful labelsclose,fix, or any linking keywords when referencing an issueJira ticket: FFL-2920