Skip to content

feat(openfeature): add semantic version operators for feature flag evaluation - #12182

Open
greghuels wants to merge 5 commits into
masterfrom
greg.huels/FFL-2920/semver-java
Open

feat(openfeature): add semantic version operators for feature flag evaluation#12182
greghuels wants to merge 5 commits into
masterfrom
greg.huels/FFL-2920/semver-java

Conversation

@greghuels

@greghuels greghuels commented Aug 11, 2026

Copy link
Copy Markdown

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 — transient semverComparand field cached during config validation.
  • ServerConfiguration — transient invalidFlags map so the evaluator returns PARSE_ERROR for flags with invalid semver comparands.
  • DDEvaluatorevaluateSemverCondition method and switch cases for all 6 operators; invalid-flag check returning PARSE_ERROR.
  • UniversalFlagConfigParser — validates and caches semver comparands during parsing; invalid comparands cause the flag to be dropped and tracked.
  • ffe-system-test-data submodule — updated to main (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

  • Unit tests: ParsedSemverTest (30 tests ported from Go semver_test.go), DDEvaluatorTest semver condition tests (28 parameterized + 2 edge cases).
  • Canonical fixture tests: 22 cases from the two system test PRs run automatically via the existing testEvaluateCanonicalFixture parameterized test and the OpenFeatureProviderSmokeTest.
  • All 551 unit tests and 250 smoke tests pass; Spotless and SpotBugs clean.

Contributor Checklist

Jira ticket: FFL-2920

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).
@datadog-prod-us1-5

datadog-prod-us1-5 Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

🎯 Code Coverage (details)
Patch Coverage: 90.86%
Overall Coverage: 74.89% (+16.92%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 21e2451 | Docs | Datadog PR Page | Give us feedback!

@dd-octo-sts

dd-octo-sts Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

🟢 Java Benchmark SLOs — All performance SLOs passed

Suite Status
Startup 🟢 pass

SLO thresholds are defined here based on automatically generated metrics. A warning is raised when results are within 5% of the threshold.

PR vs. master results
Scenario Candidate master Δ (95% CI of mean)
startup:insecure-bank:iast:Agent 14.83 s 14.64 s [+0.7%; +2.0%] (maybe worse)
startup:insecure-bank:tracing:Agent 13.58 s 13.73 s [-2.0%; -0.2%] (maybe better)
startup:petclinic:appsec:Agent 17.39 s 16.68 s [+0.0%; +8.5%] (maybe worse)
startup:petclinic:iast:Agent 17.43 s 17.48 s [-1.3%; +0.8%] (no difference)
startup:petclinic:profiling:Agent 17.42 s 16.90 s [-1.4%; +7.5%] (no difference)
startup:petclinic:sca:Agent 17.38 s 17.34 s [-0.9%; +1.4%] (no difference)
startup:petclinic:tracing:Agent 16.22 s 16.59 s [-6.4%; +1.9%] (no difference)

Commit: 21e24517 · CI Pipeline · Benchmarking Platform UI


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>
@greghuels
greghuels marked this pull request as ready for review August 11, 2026 13:16
@greghuels
greghuels requested a review from a team as a code owner August 11, 2026 13:16
@greghuels
greghuels requested review from pavlokhrebto and sameerank and removed request for a team August 11, 2026 13:16
@dd-octo-sts dd-octo-sts Bot added the tag: ai generated Largely based on code generated by an AI or LLM label Aug 11, 2026
@dd-octo-sts

dd-octo-sts Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Hi! 👋 Thanks for your pull request! 🎉

To help us review it, please make sure to:

  • Add at least one type, and one component or instrumentation label to the pull request

If you need help, please check our contributing guidelines.

@greghuels greghuels added comp: openfeature OpenFeature type: feature Enhancements and improvements labels Aug 11, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment on lines +115 to +117
if (config.invalidFlags != null && config.invalidFlags.containsKey(key)) {
return error(
defaultValue, ErrorCode.PARSE_ERROR, "invalid configuration for flag " + key);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.
@greghuels
greghuels requested a review from typotter August 11, 2026 13:44

@datadog-prod-us1-5 datadog-prod-us1-5 Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Datadog Autotest: FAIL

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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

👀

}
for (int allocIdx = 0; allocIdx < flag.allocations.size(); allocIdx++) {
final Allocation allocation = flag.allocations.get(allocIdx);
if (allocation.rules == null) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We'll address this in a follow-up PR; more ffe fixture updates coming.

Comment on lines +115 to +117
if (config.invalidFlags != null && config.invalidFlags.containsKey(key)) {
return error(
defaultValue, ErrorCode.PARSE_ERROR, "invalid configuration for flag " + key);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Only pull in updates from the git submodule that include your semver fixtures; I'll address the flag invalidate ones in a follow up.

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

Labels

comp: openfeature OpenFeature tag: ai generated Largely based on code generated by an AI or LLM type: feature Enhancements and improvements

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants