From 9f156eb9b54aab8ac16a5ab64fc13131840b7667 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Tue, 16 Jun 2026 18:49:49 -0400 Subject: [PATCH] feat(parametric/java): enable FFE span-enrichment scenario for Java Enable tests/parametric/test_ffe/test_span_enrichment.py for the Java tracer (DataDog/dd-trace-java#11658), which adds FFE APM feature-flag span enrichment behind DD_EXPERIMENTAL_FLAGGING_PROVIDER_SPAN_ENRICHMENT_ENABLED. - FeatureFlagEvaluatorController: re-activate the caller-supplied root span (span_id) around the OpenFeature evaluation so the ffe_* tags land on the test's root span. span_id arrives as a decimal string and is resolved via the OpenTracing registry (DDSpanId.from); unknown/unparsable ids skip activation and never throw. - manifests/java.yml: declare the enrichment suite at v1.64.0 (the release that ships the feature). --- manifests/java.yml | 2 +- .../FeatureFlagEvaluatorController.java | 79 ++++++++++++++----- 2 files changed, 62 insertions(+), 19 deletions(-) diff --git a/manifests/java.yml b/manifests/java.yml index 52bb4343f31..00c50b37cef 100644 --- a/manifests/java.yml +++ b/manifests/java.yml @@ -3703,7 +3703,7 @@ manifest: tests/parametric/test_dynamic_configuration.py::TestDynamicConfigV1_ServiceTargets::test_not_match_service_target: irrelevant (APMAPI-1003) tests/parametric/test_dynamic_configuration.py::TestDynamicConfigV2: v1.31.0 tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Dynamic_Evaluation: v1.56.0 - tests/parametric/test_ffe/test_span_enrichment.py: missing_feature + tests/parametric/test_ffe/test_span_enrichment.py: v1.64.0 tests/parametric/test_headers_b3.py::Test_Headers_B3::test_headers_b3_migrated_extract_invalid: # Modified by easy win activation script - declaration: missing_feature (Need to remove b3=b3multi alias) component_version: <1.58.2+06122213c8 diff --git a/utils/build/docker/java/parametric/src/main/java/com/datadoghq/trace/controller/FeatureFlagEvaluatorController.java b/utils/build/docker/java/parametric/src/main/java/com/datadoghq/trace/controller/FeatureFlagEvaluatorController.java index 4fef048ef59..08534c063c4 100644 --- a/utils/build/docker/java/parametric/src/main/java/com/datadoghq/trace/controller/FeatureFlagEvaluatorController.java +++ b/utils/build/docker/java/parametric/src/main/java/com/datadoghq/trace/controller/FeatureFlagEvaluatorController.java @@ -2,7 +2,9 @@ import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import com.datadoghq.trace.opentracing.controller.OpenTracingController; import com.fasterxml.jackson.annotation.JsonAlias; +import datadog.trace.api.DDSpanId; import datadog.trace.api.openfeature.Provider; import dev.openfeature.sdk.Client; import dev.openfeature.sdk.EvaluationContext; @@ -13,6 +15,9 @@ import dev.openfeature.sdk.ProviderState; import dev.openfeature.sdk.Structure; import dev.openfeature.sdk.Value; +import io.opentracing.Scope; +import io.opentracing.Span; +import io.opentracing.util.GlobalTracer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -78,26 +83,19 @@ public ResponseEntity> evaluate(@RequestBody final EvaluateR Object value; String reason; final EvaluationContext context = context(request); + // Re-activate the caller-supplied root span around the eval so the ffe_* tags (Phase 2) land + // on the test's span. The client sends span_id as a decimal STRING (see + // _test_client_parametric.py:814-815); the OT registry is keyed by DDSpanId.from(...). + // An unknown/missing/unparsable span_id leaves target null -> skip activation, never throw (T-01-DOS). + final Span target = resolveSpan(request.getSpanId()); try { - value = switch (request.getVariationType()) { - case "BOOLEAN" -> - client.getBooleanValue(request.getFlag(), (Boolean) request.getDefaultValue(), context); - case "STRING" -> client.getStringValue(request.getFlag(), (String) request.getDefaultValue(), context); - case "INTEGER" -> { - final Number integerEval = (Number) request.getDefaultValue(); - yield client.getIntegerValue(request.getFlag(), integerEval.intValue(), context); + if (target != null) { + try (Scope scope = GlobalTracer.get().scopeManager().activate(target)) { + value = evaluate(request, context); } - case "NUMERIC" -> { - final Number doubleEval = (Number) request.getDefaultValue(); - yield client.getDoubleValue(request.getFlag(), doubleEval.doubleValue(), context); - } - case "JSON" -> { - final Value objectValue = client.getObjectValue(request.getFlag(), Value.objectToValue(request.getDefaultValue()), context); - yield context.convertValue(objectValue); - } - default -> request.getDefaultValue(); - }; - + } else { + value = evaluate(request, context); + } reason = "DEFAULT"; } catch (Throwable e) { LOGGER.error("Error on resolution", e); @@ -110,6 +108,40 @@ public ResponseEntity> evaluate(@RequestBody final EvaluateR return ResponseEntity.ok(result); } + /** Look up a span by the (string) span_id the test client sends; null when missing/unparsable. */ + private static Span resolveSpan(final String spanId) { + if (spanId == null || spanId.isEmpty()) { + return null; + } + try { + return OpenTracingController.getSpan(DDSpanId.from(spanId)); + } catch (Throwable e) { + LOGGER.warn("Could not resolve span for span_id {}", spanId, e); + return null; + } + } + + private Object evaluate(final EvaluateRequest request, final EvaluationContext context) { + return switch (request.getVariationType()) { + case "BOOLEAN" -> + client.getBooleanValue(request.getFlag(), (Boolean) request.getDefaultValue(), context); + case "STRING" -> client.getStringValue(request.getFlag(), (String) request.getDefaultValue(), context); + case "INTEGER" -> { + final Number integerEval = (Number) request.getDefaultValue(); + yield client.getIntegerValue(request.getFlag(), integerEval.intValue(), context); + } + case "NUMERIC" -> { + final Number doubleEval = (Number) request.getDefaultValue(); + yield client.getDoubleValue(request.getFlag(), doubleEval.doubleValue(), context); + } + case "JSON" -> { + final Value objectValue = client.getObjectValue(request.getFlag(), Value.objectToValue(request.getDefaultValue()), context); + yield context.convertValue(objectValue); + } + default -> request.getDefaultValue(); + }; + } + private static EvaluationContext context(final EvaluateRequest request) { final MutableContext context = new MutableContext(); context.setTargetingKey(request.getTargetingKey()); @@ -141,12 +173,23 @@ public static class EvaluateRequest { private Object defaultValue; @JsonAlias("targeting_key") private String targetingKey; + // The test client sends span_id as a STRING (see _test_client_parametric.py:814-815). + @JsonAlias("span_id") + private String spanId; private Map attributes; public Map getAttributes() { return attributes; } + public String getSpanId() { + return spanId; + } + + public void setSpanId(String spanId) { + this.spanId = spanId; + } + public void setAttributes(Map attributes) { this.attributes = attributes; }