From 02f2a4d7fd6cbd7b2a67c39889e8b3a8d56b101c Mon Sep 17 00:00:00 2001 From: Vickie Boettcher Date: Wed, 12 Aug 2026 13:00:06 -0400 Subject: [PATCH 1/8] Populate source.name and source.version on flag_evaluations and exposures EVP FeatureFlagEvpContext.from builds the top-level context map shared by both the flagevaluation and exposures EVP writers. Add source.name ("dd-trace-java") and source.version (TracerVersion.TRACER_VERSION) so the SDK identity facets are populated on both EVP streams. This closes the gap noted in the Feature Flag Observability Telemetry Roadmap where the Java server SDK emitted no SDK/tracer name or version on the flag_evaluations EVP stream. Co-Authored-By: Claude --- .../com/datadog/featureflag/FeatureFlagEvpContext.java | 10 +++++++++- .../com/datadog/featureflag/ExposureWriterTests.java | 4 ++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagEvpContext.java b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagEvpContext.java index c964efa6c7f..84de91330bb 100644 --- a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagEvpContext.java +++ b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagEvpContext.java @@ -1,5 +1,6 @@ package com.datadog.featureflag; +import datadog.communication.ddagent.TracerVersion; import datadog.trace.api.Config; import java.util.HashMap; import java.util.Map; @@ -8,8 +9,11 @@ final class FeatureFlagEvpContext { private FeatureFlagEvpContext() {} + /** The name of the SDK emitting the feature flag evaluation/exposure EVP data. */ + private static final String SOURCE_NAME = "dd-trace-java"; + static Map from(final Config config) { - final Map context = new HashMap<>(4); + final Map context = new HashMap<>(6); context.put("service", config.getServiceName() == null ? "unknown" : config.getServiceName()); if (config.getEnv() != null) { context.put("env", config.getEnv()); @@ -17,6 +21,10 @@ static Map from(final Config config) { if (config.getVersion() != null) { context.put("version", config.getVersion()); } + // SDK identity — populates the `source.name` / `source.version` facets on the + // `flag_evaluations` and `exposures` EVP streams. See FFL-2995. + context.put("source.name", SOURCE_NAME); + context.put("source.version", TracerVersion.TRACER_VERSION); return context; } } diff --git a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/ExposureWriterTests.java b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/ExposureWriterTests.java index 76b9e2602d8..73fee60fdc8 100644 --- a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/ExposureWriterTests.java +++ b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/ExposureWriterTests.java @@ -15,6 +15,7 @@ import com.squareup.moshi.Moshi; import datadog.communication.ddagent.DDAgentFeaturesDiscovery; import datadog.communication.ddagent.SharedCommunicationObjects; +import datadog.communication.ddagent.TracerVersion; import datadog.trace.agent.test.server.http.JavaTestHttpServer; import datadog.trace.agent.test.server.http.JavaTestHttpServer.HandlerApi; import datadog.trace.api.Config; @@ -287,6 +288,9 @@ private static void assertContext( assertEquals(service == null ? "unknown" : service, context.get("service")); assertOptionalContextValue(context, "env", env); assertOptionalContextValue(context, "version", version); + // SDK identity populated by FeatureFlagEvpContext (FFL-2995). + assertEquals("dd-trace-java", context.get("source.name")); + assertEquals(TracerVersion.TRACER_VERSION, context.get("source.version")); } private static void assertOptionalContextValue( From 8ed8522f9ff7c1465532e9ca92fde949dc37ab5f Mon Sep 17 00:00:00 2001 From: Vickie Boettcher Date: Wed, 12 Aug 2026 13:12:09 -0400 Subject: [PATCH 2/8] Tighten comments: drop ticket refs and redundant javadoc --- .../java/com/datadog/featureflag/FeatureFlagEvpContext.java | 5 ++--- .../java/com/datadog/featureflag/ExposureWriterTests.java | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagEvpContext.java b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagEvpContext.java index 84de91330bb..181dab8495b 100644 --- a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagEvpContext.java +++ b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagEvpContext.java @@ -9,7 +9,6 @@ final class FeatureFlagEvpContext { private FeatureFlagEvpContext() {} - /** The name of the SDK emitting the feature flag evaluation/exposure EVP data. */ private static final String SOURCE_NAME = "dd-trace-java"; static Map from(final Config config) { @@ -21,8 +20,8 @@ static Map from(final Config config) { if (config.getVersion() != null) { context.put("version", config.getVersion()); } - // SDK identity — populates the `source.name` / `source.version` facets on the - // `flag_evaluations` and `exposures` EVP streams. See FFL-2995. + // SDK identity — populates the `source.name` / `source.version` facets on both the + // `flag_evaluations` and `exposures` EVP streams (this map is shared by both writers). context.put("source.name", SOURCE_NAME); context.put("source.version", TracerVersion.TRACER_VERSION); return context; diff --git a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/ExposureWriterTests.java b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/ExposureWriterTests.java index 73fee60fdc8..4c47133d8d7 100644 --- a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/ExposureWriterTests.java +++ b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/ExposureWriterTests.java @@ -288,7 +288,7 @@ private static void assertContext( assertEquals(service == null ? "unknown" : service, context.get("service")); assertOptionalContextValue(context, "env", env); assertOptionalContextValue(context, "version", version); - // SDK identity populated by FeatureFlagEvpContext (FFL-2995). + // SDK identity populated by FeatureFlagEvpContext. assertEquals("dd-trace-java", context.get("source.name")); assertEquals(TracerVersion.TRACER_VERSION, context.get("source.version")); } From 4aa5a6f90a25fa38a4455a3fc7930e835c9947fd Mon Sep 17 00:00:00 2001 From: Vickie Boettcher Date: Fri, 14 Aug 2026 11:34:03 -0400 Subject: [PATCH 3/8] fix(openfeature): emit source.name/version per-event, not in batch context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The flagevaluation track schema (logs-backend flagevaluation.conf) declares source.name/source.version as top-level per-event fields, siblings of flag/variant/targeting_key. The previous implementation put them in the batch context envelope alongside service/env/version, which the EVP indexer maps to context.source.* — an undeclared facet that causes the indexer to drop the entire event. Move source to the FlagEvaluationEvent top level (as a nested source object {name,version}) so it lands on the declared source.name/source.version facets. Verified end-to-end via ffe-dogfooding against staging: Java flagevaluation events now index in the staging flag_evaluations data source. Generated with Claude Code Co-Authored-By: Claude --- .../featureflag/FeatureFlagEvpContext.java | 13 +++++-------- .../featureflag/FlagEvaluationPayloads.java | 16 ++++++++++++++++ 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagEvpContext.java b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagEvpContext.java index 181dab8495b..255c2377905 100644 --- a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagEvpContext.java +++ b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagEvpContext.java @@ -1,6 +1,5 @@ package com.datadog.featureflag; -import datadog.communication.ddagent.TracerVersion; import datadog.trace.api.Config; import java.util.HashMap; import java.util.Map; @@ -9,10 +8,8 @@ final class FeatureFlagEvpContext { private FeatureFlagEvpContext() {} - private static final String SOURCE_NAME = "dd-trace-java"; - static Map from(final Config config) { - final Map context = new HashMap<>(6); + final Map context = new HashMap<>(4); context.put("service", config.getServiceName() == null ? "unknown" : config.getServiceName()); if (config.getEnv() != null) { context.put("env", config.getEnv()); @@ -20,10 +17,10 @@ static Map from(final Config config) { if (config.getVersion() != null) { context.put("version", config.getVersion()); } - // SDK identity — populates the `source.name` / `source.version` facets on both the - // `flag_evaluations` and `exposures` EVP streams (this map is shared by both writers). - context.put("source.name", SOURCE_NAME); - context.put("source.version", TracerVersion.TRACER_VERSION); + // SDK identity (source.name / source.version) is emitted per-event at the top level + // (sibling of flag/variant/targeting_key), matching the flagevaluation track schema in + // logs-backend. Putting it in the batch context would map it to context.source.*, which is + // not a declared facet and causes the indexer to drop the event. return context; } } diff --git a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FlagEvaluationPayloads.java b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FlagEvaluationPayloads.java index 3a8734fd516..95ce3e0b082 100644 --- a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FlagEvaluationPayloads.java +++ b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FlagEvaluationPayloads.java @@ -1,5 +1,6 @@ package com.datadog.featureflag; +import datadog.communication.ddagent.TracerVersion; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.Moshi; import com.squareup.moshi.Types; @@ -157,6 +158,9 @@ private byte[] toByteArray() { } } + private static final String SOURCE_NAME = "dd-trace-java"; + private static final String SOURCE_VERSION = TracerVersion.TRACER_VERSION; + static class FlagEvaluationEvent { public final long timestamp; public final FlagKeyObject flag; @@ -168,6 +172,7 @@ static class FlagEvaluationEvent { public final String targeting_key; public final Boolean runtime_default_used; public final EventContext context; + public final SourceObject source; public final ErrorObject error; FlagEvaluationEvent( @@ -196,6 +201,7 @@ static class FlagEvaluationEvent { (evaluationAttrs != null && !evaluationAttrs.isEmpty()) ? new EventContext(evaluationAttrs) : null; + this.source = new SourceObject(SOURCE_NAME, SOURCE_VERSION); this.error = (errorMessage != null && !errorMessage.isEmpty()) ? new ErrorObject(errorMessage) : null; } @@ -284,6 +290,16 @@ static class ErrorObject { } } + static class SourceObject { + public final String name; + public final String version; + + SourceObject(final String name, final String version) { + this.name = name; + this.version = version; + } + } + static class EventContext { public final Map evaluation; From a745b610c367a9ba59b9b9a67729f003ea7b2678 Mon Sep 17 00:00:00 2001 From: Vickie Boettcher Date: Thu, 27 Aug 2026 20:35:53 -0400 Subject: [PATCH 4/8] test(openfeature): verify flag evaluation source metadata Generated with Claude Code --- .../featureflag/FlagEvaluationPayloadsTest.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FlagEvaluationPayloadsTest.java b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FlagEvaluationPayloadsTest.java index d4ca517fffd..abf59462e9a 100644 --- a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FlagEvaluationPayloadsTest.java +++ b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FlagEvaluationPayloadsTest.java @@ -10,6 +10,7 @@ import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.Moshi; import com.squareup.moshi.Types; +import datadog.communication.ddagent.TracerVersion; import java.lang.reflect.Type; import java.util.Arrays; import java.util.HashMap; @@ -46,6 +47,7 @@ void fullTierPayloadUsesWorkerWireShape() throws Exception { assertObjectWithKey(ev.get("variant"), "on"); assertObjectWithKey(ev.get("allocation"), "alloc-x"); assertObjectWithKey(ev.get("flag"), "my-flag"); + assertSource(ev); final Map ctx = (Map) ev.get("context"); assertNotNull(ctx); final Map evalAttrs = (Map) ctx.get("evaluation"); @@ -222,6 +224,7 @@ void oversizedFullPayloadRowIsDegradedBeforeDrop() throws Exception { assertEquals(2.0, ((Number) ev.get("evaluation_count")).doubleValue()); assertNull(ev.get("targeting_key")); assertNull(ev.get("context")); + assertSource(ev); } @Test @@ -439,6 +442,13 @@ private static void assertObjectWithKey(final Object object, final String expect assertEquals(expectedKey, ((Map) object).get("key")); } + private static void assertSource(final Map event) { + assertTrue(event.get("source") instanceof Map); + final Map source = (Map) event.get("source"); + assertEquals("dd-trace-java", source.get("name")); + assertEquals(TracerVersion.TRACER_VERSION, source.get("version")); + } + private static String repeat(final char c, final int count) { final char[] chars = new char[count]; java.util.Arrays.fill(chars, c); From b4724e93aa90dd1e91190d80d2ee0706d1a989c0 Mon Sep 17 00:00:00 2001 From: Vickie Boettcher Date: Thu, 27 Aug 2026 20:50:07 -0400 Subject: [PATCH 5/8] fix(openfeature): send flag evaluation source headers Generated with Claude Code --- .../datadog/communication/BackendApi.java | 18 +++++++++ .../datadog/communication/EvpProxyApi.java | 19 ++++++++++ .../java/datadog/communication/IntakeApi.java | 19 ++++++++++ .../communication/EvpProxyApiTest.java | 37 ++++++++++++++++++ .../datadog/communication/IntakeApiTest.java | 34 +++++++++++++++++ .../AgentlessFeatureFlagBackendApi.java | 20 +++++++++- .../featureflag/FeatureFlagEvpPublisher.java | 21 +++++++++- .../featureflag/FlagEvaluationPayloads.java | 16 -------- .../AgentlessFeatureFlagBackendApiTest.java | 38 +++++++++++++++++++ .../featureflag/ExposureWriterTests.java | 16 +++++--- .../FeatureFlagEvpPublisherTest.java | 31 ++++++++++++++- .../FlagEvaluationPayloadsTest.java | 12 +----- .../FlagEvaluationTestSupport.java | 4 +- .../FlagEvaluationWriterImplTest.java | 26 ++++++++----- 14 files changed, 265 insertions(+), 46 deletions(-) diff --git a/communication/src/main/java/datadog/communication/BackendApi.java b/communication/src/main/java/datadog/communication/BackendApi.java index aa4385d6d75..b7b5d49eca7 100644 --- a/communication/src/main/java/datadog/communication/BackendApi.java +++ b/communication/src/main/java/datadog/communication/BackendApi.java @@ -4,6 +4,7 @@ import datadog.communication.util.IOThrowingFunction; import java.io.IOException; import java.io.InputStream; +import java.util.Map; import javax.annotation.Nullable; import okhttp3.RequestBody; @@ -17,4 +18,21 @@ T post( @Nullable OkHttpUtils.CustomListener requestListener, boolean requestCompression) throws IOException; + + /** + * Posts an HTTP request with caller-supplied headers. + * + *

The default implementation preserves compatibility with backends that do not support custom + * headers. + */ + default T post( + String uri, + RequestBody requestBody, + IOThrowingFunction responseParser, + @Nullable OkHttpUtils.CustomListener requestListener, + boolean requestCompression, + Map requestHeaders) + throws IOException { + return post(uri, requestBody, responseParser, requestListener, requestCompression); + } } diff --git a/communication/src/main/java/datadog/communication/EvpProxyApi.java b/communication/src/main/java/datadog/communication/EvpProxyApi.java index 8bb768b7e4e..2dae897dc50 100644 --- a/communication/src/main/java/datadog/communication/EvpProxyApi.java +++ b/communication/src/main/java/datadog/communication/EvpProxyApi.java @@ -1,10 +1,13 @@ package datadog.communication; +import static java.util.Collections.emptyMap; + import datadog.communication.http.HttpRetryPolicy; import datadog.communication.http.OkHttpUtils; import datadog.communication.util.IOThrowingFunction; import java.io.IOException; import java.io.InputStream; +import java.util.Map; import java.util.zip.GZIPInputStream; import javax.annotation.Nullable; import okhttp3.HttpUrl; @@ -57,6 +60,18 @@ public T post( @Nullable OkHttpUtils.CustomListener requestListener, boolean requestCompression) throws IOException { + return post(uri, requestBody, responseParser, requestListener, requestCompression, emptyMap()); + } + + @Override + public T post( + String uri, + RequestBody requestBody, + IOThrowingFunction responseParser, + @Nullable OkHttpUtils.CustomListener requestListener, + boolean requestCompression, + Map requestHeaders) + throws IOException { final HttpUrl url = evpProxyUrl.resolve(uri); Request.Builder requestBuilder = @@ -66,6 +81,10 @@ public T post( .addHeader(X_DATADOG_TRACE_ID_HEADER, traceId) .addHeader(X_DATADOG_PARENT_ID_HEADER, traceId); + for (Map.Entry header : requestHeaders.entrySet()) { + requestBuilder.addHeader(header.getKey(), header.getValue()); + } + if (requestListener != null) { requestBuilder.tag(OkHttpUtils.CustomListener.class, requestListener); } diff --git a/communication/src/main/java/datadog/communication/IntakeApi.java b/communication/src/main/java/datadog/communication/IntakeApi.java index 1a6f3f91bc7..285627381dd 100644 --- a/communication/src/main/java/datadog/communication/IntakeApi.java +++ b/communication/src/main/java/datadog/communication/IntakeApi.java @@ -1,10 +1,13 @@ package datadog.communication; +import static java.util.Collections.emptyMap; + import datadog.communication.http.HttpRetryPolicy; import datadog.communication.http.OkHttpUtils; import datadog.communication.util.IOThrowingFunction; import java.io.IOException; import java.io.InputStream; +import java.util.Map; import java.util.zip.GZIPInputStream; import javax.annotation.Nullable; import okhttp3.HttpUrl; @@ -57,6 +60,18 @@ public T post( @Nullable OkHttpUtils.CustomListener requestListener, boolean requestCompression) throws IOException { + return post(uri, requestBody, responseParser, requestListener, requestCompression, emptyMap()); + } + + @Override + public T post( + String uri, + RequestBody requestBody, + IOThrowingFunction responseParser, + @Nullable OkHttpUtils.CustomListener requestListener, + boolean requestCompression, + Map requestHeaders) + throws IOException { HttpUrl url = hostUrl.resolve(uri); Request.Builder requestBuilder = new Request.Builder() @@ -66,6 +81,10 @@ public T post( .addHeader(X_DATADOG_TRACE_ID_HEADER, traceId) .addHeader(X_DATADOG_PARENT_ID_HEADER, traceId); + for (Map.Entry header : requestHeaders.entrySet()) { + requestBuilder.addHeader(header.getKey(), header.getValue()); + } + if (requestListener != null) { requestBuilder.tag(OkHttpUtils.CustomListener.class, requestListener); } diff --git a/communication/src/test/java/datadog/communication/EvpProxyApiTest.java b/communication/src/test/java/datadog/communication/EvpProxyApiTest.java index 14c6962bf8e..79adb0fa3cd 100644 --- a/communication/src/test/java/datadog/communication/EvpProxyApiTest.java +++ b/communication/src/test/java/datadog/communication/EvpProxyApiTest.java @@ -1,10 +1,13 @@ package datadog.communication; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import datadog.communication.http.HttpRetryPolicy; import java.io.IOException; +import java.util.HashMap; +import java.util.Map; import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.RequestBody; @@ -61,5 +64,39 @@ void reportsHttpStatusForRejectedRequest() throws Exception { final RecordedRequest request = server.takeRequest(); assertEquals("/evp_proxy/v4/api/v2/exposures", request.getPath()); assertEquals("event-platform-intake", request.getHeader("X-Datadog-EVP-Subdomain")); + assertNull(request.getHeader("DD-EVP-ORIGIN")); + assertNull(request.getHeader("DD-EVP-ORIGIN-VERSION")); + } + + @Test + void addsCustomHeadersWithoutReplacingEvpHeaders() throws Exception { + server.enqueue(new MockResponse().setResponseCode(200)); + final EvpProxyApi api = + new EvpProxyApi( + "123", + server.url("/evp_proxy/v4/"), + "event-platform-intake", + HttpRetryPolicy.Factory.NEVER_RETRY, + client, + false); + final Map requestHeaders = new HashMap<>(); + requestHeaders.put("DD-EVP-ORIGIN", "dd-trace-java"); + requestHeaders.put("DD-EVP-ORIGIN-VERSION", "1.2.3"); + + api.post( + "flagevaluation", + RequestBody.create(MediaType.parse("application/json"), "{}"), + stream -> null, + null, + false, + requestHeaders); + + final RecordedRequest request = server.takeRequest(); + assertEquals("/evp_proxy/v4/api/v2/flagevaluation", request.getPath()); + assertEquals("event-platform-intake", request.getHeader("X-Datadog-EVP-Subdomain")); + assertEquals("123", request.getHeader("x-datadog-trace-id")); + assertEquals("123", request.getHeader("x-datadog-parent-id")); + assertEquals("dd-trace-java", request.getHeader("DD-EVP-ORIGIN")); + assertEquals("1.2.3", request.getHeader("DD-EVP-ORIGIN-VERSION")); } } diff --git a/communication/src/test/java/datadog/communication/IntakeApiTest.java b/communication/src/test/java/datadog/communication/IntakeApiTest.java index 326cf21ca84..0f2f1d552c7 100644 --- a/communication/src/test/java/datadog/communication/IntakeApiTest.java +++ b/communication/src/test/java/datadog/communication/IntakeApiTest.java @@ -4,6 +4,8 @@ import datadog.communication.http.HttpRetryPolicy; import java.io.IOException; +import java.util.HashMap; +import java.util.Map; import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.RequestBody; @@ -45,6 +47,38 @@ void requestsIdentityResponseEncodingWhenCompressionIsDisabled() throws Exceptio assertEquals("identity", postAndReadAcceptEncoding(false)); } + @Test + void addsCustomHeadersWithoutReplacingIntakeHeaders() throws Exception { + server.enqueue(new MockResponse().setResponseCode(200).setBody("{}")); + final IntakeApi api = + new IntakeApi( + server.url("/api/v2/"), + "api-key", + "123", + HttpRetryPolicy.Factory.NEVER_RETRY, + client, + false); + final Map requestHeaders = new HashMap<>(); + requestHeaders.put("DD-EVP-ORIGIN", "dd-trace-java"); + requestHeaders.put("DD-EVP-ORIGIN-VERSION", "1.2.3"); + + api.post( + "flagevaluation", + RequestBody.create(JSON, "{}"), + responseBody -> null, + null, + false, + requestHeaders); + + final RecordedRequest request = server.takeRequest(); + assertEquals("/api/v2/flagevaluation", request.getPath()); + assertEquals("api-key", request.getHeader("dd-api-key")); + assertEquals("123", request.getHeader("x-datadog-trace-id")); + assertEquals("123", request.getHeader("x-datadog-parent-id")); + assertEquals("dd-trace-java", request.getHeader("DD-EVP-ORIGIN")); + assertEquals("1.2.3", request.getHeader("DD-EVP-ORIGIN-VERSION")); + } + private String postAndReadAcceptEncoding(final boolean responseCompression) throws Exception { server.enqueue(new MockResponse().setResponseCode(200).setBody("{}")); final IntakeApi api = diff --git a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/AgentlessFeatureFlagBackendApi.java b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/AgentlessFeatureFlagBackendApi.java index 769ebfd1dd1..45e08a37594 100644 --- a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/AgentlessFeatureFlagBackendApi.java +++ b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/AgentlessFeatureFlagBackendApi.java @@ -1,5 +1,7 @@ package com.datadog.featureflag; +import static java.util.Collections.emptyMap; + import datadog.communication.BackendApi; import datadog.communication.HttpResponseException; import datadog.communication.http.OkHttpUtils; @@ -7,6 +9,7 @@ import java.io.IOException; import java.io.InputStream; import java.net.ConnectException; +import java.util.Map; import java.util.function.Supplier; import javax.annotation.Nullable; import okhttp3.RequestBody; @@ -43,10 +46,22 @@ public T post( @Nullable final OkHttpUtils.CustomListener requestListener, final boolean requestCompression) throws IOException { + return post(uri, requestBody, responseParser, requestListener, requestCompression, emptyMap()); + } + + @Override + public T post( + final String uri, + final RequestBody requestBody, + final IOThrowingFunction responseParser, + @Nullable final OkHttpUtils.CustomListener requestListener, + final boolean requestCompression, + final Map requestHeaders) + throws IOException { final BackendApi selectedApi = activeApi; try { return selectedApi.post( - uri, requestBody, responseParser, requestListener, requestCompression); + uri, requestBody, responseParser, requestListener, requestCompression, requestHeaders); } catch (final IOException exception) { if (selectedApi != proxyApi || !isDefinitiveRejection(exception)) { throw exception; @@ -56,7 +71,8 @@ public T post( if (directApi == null) { throw exception; } - return directApi.post(uri, requestBody, responseParser, requestListener, requestCompression); + return directApi.post( + uri, requestBody, responseParser, requestListener, requestCompression, requestHeaders); } } diff --git a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagEvpPublisher.java b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagEvpPublisher.java index 8a024b52f43..9543317568f 100644 --- a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagEvpPublisher.java +++ b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagEvpPublisher.java @@ -1,12 +1,18 @@ package com.datadog.featureflag; +import static java.util.Collections.emptyMap; +import static java.util.Collections.unmodifiableMap; + import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.Moshi; import datadog.communication.BackendApi; import datadog.communication.BackendApiFactory; +import datadog.communication.ddagent.TracerVersion; import datadog.trace.api.intake.Intake; import java.io.IOException; import java.io.UnsupportedEncodingException; +import java.util.HashMap; +import java.util.Map; import java.util.function.Supplier; import okhttp3.MediaType; import okhttp3.RequestBody; @@ -14,6 +20,8 @@ final class FeatureFlagEvpPublisher { private static final MediaType JSON = MediaType.parse("application/json"); + private static final String FLAG_EVALUATION_ROUTE = "flagevaluation"; + private static final Map FLAG_EVALUATION_HEADERS = flagEvaluationHeaders(); private final Supplier backendApiSupplier; private final JsonAdapter jsonAdapter; @@ -58,7 +66,18 @@ void post(final String route, final byte[] json) throws IOException { throw new IllegalStateException("EVP Proxy not available"); } final RequestBody requestBody = RequestBody.create(JSON, json); - evp.post(route, requestBody, stream -> null, null, false); + evp.post(route, requestBody, stream -> null, null, false, requestHeaders(route)); + } + + private static Map requestHeaders(final String route) { + return FLAG_EVALUATION_ROUTE.equals(route) ? FLAG_EVALUATION_HEADERS : emptyMap(); + } + + private static Map flagEvaluationHeaders() { + final Map headers = new HashMap<>(2); + headers.put("DD-EVP-ORIGIN", "dd-trace-java"); + headers.put("DD-EVP-ORIGIN-VERSION", TracerVersion.TRACER_VERSION); + return unmodifiableMap(headers); } static byte[] utf8Bytes(final String json) { diff --git a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FlagEvaluationPayloads.java b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FlagEvaluationPayloads.java index 95ce3e0b082..3a8734fd516 100644 --- a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FlagEvaluationPayloads.java +++ b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FlagEvaluationPayloads.java @@ -1,6 +1,5 @@ package com.datadog.featureflag; -import datadog.communication.ddagent.TracerVersion; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.Moshi; import com.squareup.moshi.Types; @@ -158,9 +157,6 @@ private byte[] toByteArray() { } } - private static final String SOURCE_NAME = "dd-trace-java"; - private static final String SOURCE_VERSION = TracerVersion.TRACER_VERSION; - static class FlagEvaluationEvent { public final long timestamp; public final FlagKeyObject flag; @@ -172,7 +168,6 @@ static class FlagEvaluationEvent { public final String targeting_key; public final Boolean runtime_default_used; public final EventContext context; - public final SourceObject source; public final ErrorObject error; FlagEvaluationEvent( @@ -201,7 +196,6 @@ static class FlagEvaluationEvent { (evaluationAttrs != null && !evaluationAttrs.isEmpty()) ? new EventContext(evaluationAttrs) : null; - this.source = new SourceObject(SOURCE_NAME, SOURCE_VERSION); this.error = (errorMessage != null && !errorMessage.isEmpty()) ? new ErrorObject(errorMessage) : null; } @@ -290,16 +284,6 @@ static class ErrorObject { } } - static class SourceObject { - public final String name; - public final String version; - - SourceObject(final String name, final String version) { - this.name = name; - this.version = version; - } - } - static class EventContext { public final Map evaluation; diff --git a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/AgentlessFeatureFlagBackendApiTest.java b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/AgentlessFeatureFlagBackendApiTest.java index 117c72ba2a1..6d57b3e5cba 100644 --- a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/AgentlessFeatureFlagBackendApiTest.java +++ b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/AgentlessFeatureFlagBackendApiTest.java @@ -1,5 +1,7 @@ package com.datadog.featureflag; +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonMap; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -15,6 +17,7 @@ import java.net.SocketTimeoutException; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Stream; import javax.annotation.Nullable; @@ -58,6 +61,22 @@ void replaysRejectedBatchDirectlyAndKeepsDirectRoute(final int statusCode) throw assertSame(secondBody, direct.requestBodies.get(1)); } + @Test + void preservesRequestHeadersWhenReplayingRejectedBatch() throws Exception { + final RecordingBackendApi local = + new RecordingBackendApi(new HttpResponseException(404, "rejected")); + final RecordingBackendApi direct = new RecordingBackendApi(); + final AgentlessFeatureFlagBackendApi api = + new AgentlessFeatureFlagBackendApi(local, () -> direct, "flag evaluation"); + final Map requestHeaders = singletonMap("DD-EVP-ORIGIN", "dd-trace-java"); + + api.post( + "flagevaluation", requestBody("evaluation"), stream -> null, null, false, requestHeaders); + + assertSame(requestHeaders, local.requestHeaders.get(0)); + assertSame(requestHeaders, direct.requestHeaders.get(0)); + } + @ParameterizedTest @MethodSource("featureFlagRoutes") void fallsBackAfterConnectionRefusal(final String route, final String eventType) @@ -167,6 +186,7 @@ private static Stream featureFlagRoutes() { private static final class RecordingBackendApi implements BackendApi { private IOException failure; private final List requestBodies = new ArrayList<>(); + private final List> requestHeaders = new ArrayList<>(); private int calls; private RecordingBackendApi() { @@ -185,8 +205,26 @@ public T post( @Nullable final OkHttpUtils.CustomListener requestListener, final boolean requestCompression) throws IOException { + return record(requestBody, emptyMap()); + } + + @Override + public T post( + final String uri, + final RequestBody requestBody, + final IOThrowingFunction responseParser, + @Nullable final OkHttpUtils.CustomListener requestListener, + final boolean requestCompression, + final Map requestHeaders) + throws IOException { + return record(requestBody, requestHeaders); + } + + private T record(final RequestBody requestBody, final Map requestHeaders) + throws IOException { calls++; requestBodies.add(requestBody); + this.requestHeaders.add(requestHeaders); if (failure != null) { throw failure; } diff --git a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/ExposureWriterTests.java b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/ExposureWriterTests.java index daaf213ebfb..0c22b61bcf8 100644 --- a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/ExposureWriterTests.java +++ b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/ExposureWriterTests.java @@ -10,6 +10,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyMap; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -313,7 +314,7 @@ void testAmbiguousExposureBatchIsNotRetriedOrReplayedDirectly() throws Exception .thenReturn(proxyApi); when(backendApiFactory.createDirectIntakeApi(Intake.EVENT_PLATFORM, true)) .thenReturn(directApi); - when(proxyApi.post(eq("exposures"), any(RequestBody.class), any(), any(), eq(false))) + when(proxyApi.post(eq("exposures"), any(RequestBody.class), any(), any(), eq(false), anyMap())) .thenThrow(new SocketTimeoutException("ambiguous timeout")) .thenThrow(new ConnectException("definitive refusal")); final FeatureFlagBackendApiFactory featureFlagBackendApiFactory = @@ -328,21 +329,24 @@ void testAmbiguousExposureBatchIsNotRetriedOrReplayedDirectly() throws Exception poll.eventually( () -> verify(proxyApi) - .post(eq("exposures"), any(RequestBody.class), any(), any(), eq(false))); + .post( + eq("exposures"), any(RequestBody.class), any(), any(), eq(false), anyMap())); MILLISECONDS.sleep(300); verify(proxyApi, times(1)) - .post(eq("exposures"), any(RequestBody.class), any(), any(), eq(false)); + .post(eq("exposures"), any(RequestBody.class), any(), any(), eq(false), anyMap()); verify(directApi, never()) - .post(eq("exposures"), any(RequestBody.class), any(), any(), eq(false)); + .post(eq("exposures"), any(RequestBody.class), any(), any(), eq(false), anyMap()); writer.accept(exposures.get(1)); poll.eventually( () -> verify(directApi) - .post(eq("exposures"), any(RequestBody.class), any(), any(), eq(false))); + .post( + eq("exposures"), any(RequestBody.class), any(), any(), eq(false), anyMap())); final ArgumentCaptor directBody = ArgumentCaptor.forClass(RequestBody.class); - verify(directApi).post(eq("exposures"), directBody.capture(), any(), any(), eq(false)); + verify(directApi) + .post(eq("exposures"), directBody.capture(), any(), any(), eq(false), anyMap()); final Buffer buffer = new Buffer(); directBody.getValue().writeTo(buffer); final ExposuresRequest directRequest = diff --git a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FeatureFlagEvpPublisherTest.java b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FeatureFlagEvpPublisherTest.java index 379dd49e444..4d94a982ad8 100644 --- a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FeatureFlagEvpPublisherTest.java +++ b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FeatureFlagEvpPublisherTest.java @@ -1,5 +1,6 @@ package com.datadog.featureflag; +import static java.util.Collections.emptyMap; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; @@ -12,7 +13,10 @@ import datadog.communication.BackendApi; import datadog.communication.BackendApiFactory; +import datadog.communication.ddagent.TracerVersion; import datadog.trace.api.intake.Intake; +import java.util.HashMap; +import java.util.Map; import okhttp3.RequestBody; import org.junit.jupiter.api.Test; @@ -46,7 +50,25 @@ void responseCompressionCanBeDisabled() throws Exception { verify(factory).createBackendApi(Intake.EVENT_PLATFORM, false); verify(backendApi) - .post(eq("flagevaluation"), any(RequestBody.class), any(), isNull(), eq(false)); + .post( + eq("flagevaluation"), + any(RequestBody.class), + any(), + isNull(), + eq(false), + eq(flagEvaluationHeaders())); + } + + @Test + void exposureRequestsDoNotIncludeFlagEvaluationHeaders() throws Exception { + final BackendApi backendApi = mock(BackendApi.class); + final FeatureFlagEvpPublisher publisher = + new FeatureFlagEvpPublisher<>(() -> backendApi, TestRequest.class); + + publisher.post("exposures", new TestRequest("value")); + + verify(backendApi) + .post(eq("exposures"), any(RequestBody.class), any(), isNull(), eq(false), eq(emptyMap())); } @Test @@ -61,6 +83,13 @@ void postThrowsWhenEvpBackendApiCannotBeCreated() { () -> publisher.post("flagevaluation", FeatureFlagEvpPublisher.utf8Bytes("{}"))); } + private static Map flagEvaluationHeaders() { + final Map headers = new HashMap<>(); + headers.put("DD-EVP-ORIGIN", "dd-trace-java"); + headers.put("DD-EVP-ORIGIN-VERSION", TracerVersion.TRACER_VERSION); + return headers; + } + static class TestRequest { public final String value; diff --git a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FlagEvaluationPayloadsTest.java b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FlagEvaluationPayloadsTest.java index abf59462e9a..9c5ed201e0c 100644 --- a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FlagEvaluationPayloadsTest.java +++ b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FlagEvaluationPayloadsTest.java @@ -10,7 +10,6 @@ import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.Moshi; import com.squareup.moshi.Types; -import datadog.communication.ddagent.TracerVersion; import java.lang.reflect.Type; import java.util.Arrays; import java.util.HashMap; @@ -47,13 +46,13 @@ void fullTierPayloadUsesWorkerWireShape() throws Exception { assertObjectWithKey(ev.get("variant"), "on"); assertObjectWithKey(ev.get("allocation"), "alloc-x"); assertObjectWithKey(ev.get("flag"), "my-flag"); - assertSource(ev); final Map ctx = (Map) ev.get("context"); assertNotNull(ctx); final Map evalAttrs = (Map) ctx.get("evaluation"); assertNotNull(evalAttrs); assertEquals("us-east-1", evalAttrs.get("region")); assertFalse(ev.containsKey("reason")); + assertFalse(ev.containsKey("source")); } @Test @@ -98,6 +97,7 @@ void degradedTierEventOmitsTargetingKeyAndContext() throws Exception { final Map ev = firstEvent(json); assertNull(ev.get("targeting_key")); assertNull(ev.get("context")); + assertFalse(ev.containsKey("source")); } @Test @@ -224,7 +224,6 @@ void oversizedFullPayloadRowIsDegradedBeforeDrop() throws Exception { assertEquals(2.0, ((Number) ev.get("evaluation_count")).doubleValue()); assertNull(ev.get("targeting_key")); assertNull(ev.get("context")); - assertSource(ev); } @Test @@ -442,13 +441,6 @@ private static void assertObjectWithKey(final Object object, final String expect assertEquals(expectedKey, ((Map) object).get("key")); } - private static void assertSource(final Map event) { - assertTrue(event.get("source") instanceof Map); - final Map source = (Map) event.get("source"); - assertEquals("dd-trace-java", source.get("name")); - assertEquals(TracerVersion.TRACER_VERSION, source.get("version")); - } - private static String repeat(final char c, final int count) { final char[] chars = new char[count]; java.util.Arrays.fill(chars, c); diff --git a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FlagEvaluationTestSupport.java b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FlagEvaluationTestSupport.java index 502f0e76c69..b1f94fc0e3e 100644 --- a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FlagEvaluationTestSupport.java +++ b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FlagEvaluationTestSupport.java @@ -6,6 +6,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyMap; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -132,7 +133,8 @@ static CapturedJson flushAndCapture(final TestWriterSetup setup) throws Exceptio static List flushAndCaptureAll(final TestWriterSetup setup) throws Exception { final List captured = new ArrayList<>(); - when(setup.mockEvp.post(eq("flagevaluation"), any(RequestBody.class), any(), any(), eq(false))) + when(setup.mockEvp.post( + eq("flagevaluation"), any(RequestBody.class), any(), any(), eq(false), anyMap())) .thenAnswer( inv -> { captured.add(inv.getArgument(1)); diff --git a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FlagEvaluationWriterImplTest.java b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FlagEvaluationWriterImplTest.java index ba3d03ec6d5..da19e448dd2 100644 --- a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FlagEvaluationWriterImplTest.java +++ b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FlagEvaluationWriterImplTest.java @@ -22,6 +22,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyMap; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doAnswer; @@ -377,7 +378,8 @@ void finalFlushRunsWithoutTheInterruptFlagSet() throws Exception { final java.util.concurrent.CountDownLatch posted = new java.util.concurrent.CountDownLatch(1); final boolean[] interruptedDuringPost = {true}; final BackendApi mockEvp = mock(BackendApi.class); - when(mockEvp.post(eq("flagevaluation"), any(RequestBody.class), any(), any(), eq(false))) + when(mockEvp.post( + eq("flagevaluation"), any(RequestBody.class), any(), any(), eq(false), anyMap())) .thenAnswer( inv -> { interruptedDuringPost[0] = Thread.currentThread().isInterrupted(); @@ -406,7 +408,8 @@ void closeDrainsAndFinalFlushesQueuedEvents() throws Exception { final java.util.concurrent.CountDownLatch posted = new java.util.concurrent.CountDownLatch(1); final RequestBody[] captured = {null}; final BackendApi mockEvp = mock(BackendApi.class); - when(mockEvp.post(eq("flagevaluation"), any(RequestBody.class), any(), any(), eq(false))) + when(mockEvp.post( + eq("flagevaluation"), any(RequestBody.class), any(), any(), eq(false), anyMap())) .thenAnswer( inv -> { captured[0] = inv.getArgument(1); @@ -448,7 +451,8 @@ void continuousTrafficFlushesWithoutWaitingForIdle() throws Exception { writer.enqueue(simpleEvent("busy-flag", "on")); try { verify(mockEvp, atLeastOnce()) - .post(eq("flagevaluation"), any(RequestBody.class), any(), any(), eq(false)); + .post( + eq("flagevaluation"), any(RequestBody.class), any(), any(), eq(false), anyMap()); posted = true; break; } catch (AssertionError ignored) { @@ -472,7 +476,8 @@ void flushPostsToFlagevaluationEndpoint() throws Exception { setup.handler.flush(); verify(setup.factory).createBackendApi(Intake.EVENT_PLATFORM, false); - verify(mockEvp).post(eq("flagevaluation"), any(RequestBody.class), any(), any(), eq(false)); + verify(mockEvp) + .post(eq("flagevaluation"), any(RequestBody.class), any(), any(), eq(false), anyMap()); } @Test @@ -489,7 +494,7 @@ void splitPostFailureDoesNotRetryAlreadySentPayloads() throws Exception { return null; }) .when(mockEvp) - .post(eq("flagevaluation"), any(RequestBody.class), any(), any(), eq(false)); + .post(eq("flagevaluation"), any(RequestBody.class), any(), any(), eq(false), anyMap()); for (int i = 0; i < 4; i++) { final Map attrs = new HashMap<>(); @@ -555,7 +560,8 @@ void eventConsentFalseStaysHashedEvenWhenGatewayLaterReportsTrue() throws Except setup.handler.drainAndAggregate(); final java.util.List captured = new java.util.ArrayList<>(); - when(mockEvp.post(eq("flagevaluation"), any(RequestBody.class), any(), any(), eq(false))) + when(mockEvp.post( + eq("flagevaluation"), any(RequestBody.class), any(), any(), eq(false), anyMap())) .thenAnswer( inv -> { captured.add(inv.getArgument(1)); @@ -588,7 +594,8 @@ void eventConsentTrueStaysRawEvenWhenGatewayLaterReportsFalse() throws Exception setup.handler.drainAndAggregate(); final java.util.List captured = new java.util.ArrayList<>(); - when(mockEvp.post(eq("flagevaluation"), any(RequestBody.class), any(), any(), eq(false))) + when(mockEvp.post( + eq("flagevaluation"), any(RequestBody.class), any(), any(), eq(false), anyMap())) .thenAnswer( inv -> { captured.add(inv.getArgument(1)); @@ -654,13 +661,14 @@ void encodeFailureClearsAggregatorSoLaterFlushesRecover() throws Exception { setup.handler.drainAndAggregate(); setup.handler.flush(); verify(mockEvp, org.mockito.Mockito.never()) - .post(eq("flagevaluation"), any(RequestBody.class), any(), any(), eq(false)); + .post(eq("flagevaluation"), any(RequestBody.class), any(), any(), eq(false), anyMap()); // The bucket must not survive the failed flush. A follow-up healthy event flushes cleanly. setup.handler.add(simpleEvent("healthy-flag", "on")); setup.handler.drainAndAggregate(); setup.handler.flush(); - verify(mockEvp).post(eq("flagevaluation"), any(RequestBody.class), any(), any(), eq(false)); + verify(mockEvp) + .post(eq("flagevaluation"), any(RequestBody.class), any(), any(), eq(false), anyMap()); } @Test From 38e9e1b4159384989d1f2c01f8339e4cb07cd3b5 Mon Sep 17 00:00:00 2001 From: Vickie Boettcher Date: Thu, 27 Aug 2026 21:34:07 -0400 Subject: [PATCH 6/8] test(communication): cover backend header fallback Generated with Claude Code --- .../datadog/communication/BackendApiTest.java | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 communication/src/test/java/datadog/communication/BackendApiTest.java diff --git a/communication/src/test/java/datadog/communication/BackendApiTest.java b/communication/src/test/java/datadog/communication/BackendApiTest.java new file mode 100644 index 00000000000..225892e847b --- /dev/null +++ b/communication/src/test/java/datadog/communication/BackendApiTest.java @@ -0,0 +1,45 @@ +package datadog.communication; + +import static java.util.Collections.singletonMap; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import datadog.communication.http.OkHttpUtils; +import datadog.communication.util.IOThrowingFunction; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import javax.annotation.Nullable; +import okhttp3.RequestBody; +import org.junit.jupiter.api.Test; + +class BackendApiTest { + + @Test + void defaultPostFallsBackToPostWithoutHeaders() throws IOException { + final BackendApi api = new TestBackendApi(); + + final String response = + api.post( + "flagevaluation", + null, + input -> "response", + null, + false, + singletonMap("DD-EVP-ORIGIN", "dd-trace-java")); + + assertEquals("response", response); + } + + private static final class TestBackendApi implements BackendApi { + @Override + public T post( + final String uri, + final RequestBody requestBody, + final IOThrowingFunction responseParser, + @Nullable final OkHttpUtils.CustomListener requestListener, + final boolean requestCompression) + throws IOException { + return responseParser.apply(new ByteArrayInputStream(new byte[0])); + } + } +} From 3b940be549e83e9a8b13c8c6585693a8c3be90a9 Mon Sep 17 00:00:00 2001 From: Vickie Boettcher Date: Thu, 27 Aug 2026 21:48:37 -0400 Subject: [PATCH 7/8] refactor(openfeature): configure SDK headers per backend Send SDK identity headers on both flag evaluation and exposure requests while preserving them across proxy-to-direct fallback. Keep the BackendApi request signature unchanged by configuring headers on feature-flag backend instances.\n\nGenerated with Claude Code --- .../datadog/communication/BackendApi.java | 18 -------- .../communication/BackendApiFactory.java | 20 ++++++++- .../datadog/communication/EvpProxyApi.java | 34 +++++++++----- .../java/datadog/communication/IntakeApi.java | 27 ++++++----- .../communication/BackendApiFactoryTest.java | 6 ++- .../datadog/communication/BackendApiTest.java | 45 ------------------- .../communication/EvpProxyApiTest.java | 37 --------------- .../datadog/communication/IntakeApiTest.java | 25 +++-------- .../AgentlessFeatureFlagBackendApi.java | 20 +-------- .../FeatureFlagBackendApiFactory.java | 17 ++++++- .../featureflag/FeatureFlagEvpPublisher.java | 21 +-------- .../AgentlessFeatureFlagBackendApiTest.java | 38 ---------------- .../featureflag/ExposureWriterTests.java | 16 +++---- .../FeatureFlagBackendApiFactoryTest.java | 11 +++++ .../FeatureFlagEvpPublisherTest.java | 31 +------------ .../FlagEvaluationPayloadsTest.java | 2 - .../FlagEvaluationTestSupport.java | 4 +- .../FlagEvaluationWriterImplTest.java | 26 ++++------- 18 files changed, 113 insertions(+), 285 deletions(-) delete mode 100644 communication/src/test/java/datadog/communication/BackendApiTest.java diff --git a/communication/src/main/java/datadog/communication/BackendApi.java b/communication/src/main/java/datadog/communication/BackendApi.java index b7b5d49eca7..aa4385d6d75 100644 --- a/communication/src/main/java/datadog/communication/BackendApi.java +++ b/communication/src/main/java/datadog/communication/BackendApi.java @@ -4,7 +4,6 @@ import datadog.communication.util.IOThrowingFunction; import java.io.IOException; import java.io.InputStream; -import java.util.Map; import javax.annotation.Nullable; import okhttp3.RequestBody; @@ -18,21 +17,4 @@ T post( @Nullable OkHttpUtils.CustomListener requestListener, boolean requestCompression) throws IOException; - - /** - * Posts an HTTP request with caller-supplied headers. - * - *

The default implementation preserves compatibility with backends that do not support custom - * headers. - */ - default T post( - String uri, - RequestBody requestBody, - IOThrowingFunction responseParser, - @Nullable OkHttpUtils.CustomListener requestListener, - boolean requestCompression, - Map requestHeaders) - throws IOException { - return post(uri, requestBody, responseParser, requestListener, requestCompression); - } } diff --git a/communication/src/main/java/datadog/communication/BackendApiFactory.java b/communication/src/main/java/datadog/communication/BackendApiFactory.java index 2ac0447fc7d..347b5d4c42d 100644 --- a/communication/src/main/java/datadog/communication/BackendApiFactory.java +++ b/communication/src/main/java/datadog/communication/BackendApiFactory.java @@ -1,11 +1,16 @@ package datadog.communication; +import static java.util.Collections.emptyMap; +import static java.util.Collections.unmodifiableMap; + import datadog.communication.ddagent.DDAgentFeaturesDiscovery; import datadog.communication.ddagent.SharedCommunicationObjects; import datadog.communication.http.HttpRetryPolicy; import datadog.trace.api.Config; import datadog.trace.api.intake.Intake; import datadog.trace.util.throwable.FatalAgentMisconfigurationError; +import java.util.HashMap; +import java.util.Map; import javax.annotation.Nullable; import okhttp3.HttpUrl; import org.slf4j.Logger; @@ -17,10 +22,19 @@ public class BackendApiFactory { private final Config config; private final SharedCommunicationObjects sharedCommunicationObjects; + private final Map requestHeaders; public BackendApiFactory(Config config, SharedCommunicationObjects sharedCommunicationObjects) { + this(config, sharedCommunicationObjects, emptyMap()); + } + + public BackendApiFactory( + Config config, + SharedCommunicationObjects sharedCommunicationObjects, + Map requestHeaders) { this.config = config; this.sharedCommunicationObjects = sharedCommunicationObjects; + this.requestHeaders = unmodifiableMap(new HashMap<>(requestHeaders)); } public @Nullable BackendApi createBackendApi(Intake intake) { @@ -61,7 +75,8 @@ public BackendApi createDirectIntakeApi(Intake intake, boolean responseCompressi traceId, retryPolicyFactory(), sharedCommunicationObjects.getIntakeHttpClient(), - responseCompression); + responseCompression, + requestHeaders); } /** Creates an API client that uses the specified retry policy with a compatible local proxy. */ @@ -99,7 +114,8 @@ public BackendApi createDirectIntakeApi(Intake intake, boolean responseCompressi subdomain, retryPolicyFactory, sharedCommunicationObjects.agentHttpClient, - responseCompression); + responseCompression, + requestHeaders); } private static HttpRetryPolicy.Factory retryPolicyFactory() { diff --git a/communication/src/main/java/datadog/communication/EvpProxyApi.java b/communication/src/main/java/datadog/communication/EvpProxyApi.java index 2dae897dc50..48adf199739 100644 --- a/communication/src/main/java/datadog/communication/EvpProxyApi.java +++ b/communication/src/main/java/datadog/communication/EvpProxyApi.java @@ -1,12 +1,14 @@ package datadog.communication; import static java.util.Collections.emptyMap; +import static java.util.Collections.unmodifiableMap; import datadog.communication.http.HttpRetryPolicy; import datadog.communication.http.OkHttpUtils; import datadog.communication.util.IOThrowingFunction; import java.io.IOException; import java.io.InputStream; +import java.util.HashMap; import java.util.Map; import java.util.zip.GZIPInputStream; import javax.annotation.Nullable; @@ -36,6 +38,7 @@ public class EvpProxyApi implements BackendApi { private final String subdomain; private final OkHttpClient httpClient; private final boolean responseCompression; + private final Map requestHeaders; public EvpProxyApi( String traceId, @@ -44,12 +47,31 @@ public EvpProxyApi( HttpRetryPolicy.Factory retryPolicyFactory, OkHttpClient httpClient, boolean responseCompression) { + this( + traceId, + evpProxyUrl, + subdomain, + retryPolicyFactory, + httpClient, + responseCompression, + emptyMap()); + } + + public EvpProxyApi( + String traceId, + HttpUrl evpProxyUrl, + String subdomain, + HttpRetryPolicy.Factory retryPolicyFactory, + OkHttpClient httpClient, + boolean responseCompression, + Map requestHeaders) { this.traceId = traceId; this.evpProxyUrl = evpProxyUrl.resolve("api/" + API_VERSION + "/"); this.subdomain = subdomain; this.retryPolicyFactory = retryPolicyFactory; this.httpClient = httpClient; this.responseCompression = responseCompression; + this.requestHeaders = unmodifiableMap(new HashMap<>(requestHeaders)); } @Override @@ -60,18 +82,6 @@ public T post( @Nullable OkHttpUtils.CustomListener requestListener, boolean requestCompression) throws IOException { - return post(uri, requestBody, responseParser, requestListener, requestCompression, emptyMap()); - } - - @Override - public T post( - String uri, - RequestBody requestBody, - IOThrowingFunction responseParser, - @Nullable OkHttpUtils.CustomListener requestListener, - boolean requestCompression, - Map requestHeaders) - throws IOException { final HttpUrl url = evpProxyUrl.resolve(uri); Request.Builder requestBuilder = diff --git a/communication/src/main/java/datadog/communication/IntakeApi.java b/communication/src/main/java/datadog/communication/IntakeApi.java index 285627381dd..fa431a29786 100644 --- a/communication/src/main/java/datadog/communication/IntakeApi.java +++ b/communication/src/main/java/datadog/communication/IntakeApi.java @@ -1,12 +1,14 @@ package datadog.communication; import static java.util.Collections.emptyMap; +import static java.util.Collections.unmodifiableMap; import datadog.communication.http.HttpRetryPolicy; import datadog.communication.http.OkHttpUtils; import datadog.communication.util.IOThrowingFunction; import java.io.IOException; import java.io.InputStream; +import java.util.HashMap; import java.util.Map; import java.util.zip.GZIPInputStream; import javax.annotation.Nullable; @@ -36,6 +38,7 @@ public class IntakeApi implements BackendApi { private final boolean responseCompression; private final HttpUrl hostUrl; private final OkHttpClient httpClient; + private final Map requestHeaders; public IntakeApi( HttpUrl hostUrl, @@ -44,12 +47,24 @@ public IntakeApi( HttpRetryPolicy.Factory retryPolicyFactory, OkHttpClient httpClient, boolean responseCompression) { + this(hostUrl, apiKey, traceId, retryPolicyFactory, httpClient, responseCompression, emptyMap()); + } + + public IntakeApi( + HttpUrl hostUrl, + String apiKey, + String traceId, + HttpRetryPolicy.Factory retryPolicyFactory, + OkHttpClient httpClient, + boolean responseCompression, + Map requestHeaders) { this.hostUrl = hostUrl; this.apiKey = apiKey; this.traceId = traceId; this.retryPolicyFactory = retryPolicyFactory; this.responseCompression = responseCompression; this.httpClient = httpClient; + this.requestHeaders = unmodifiableMap(new HashMap<>(requestHeaders)); } @Override @@ -60,18 +75,6 @@ public T post( @Nullable OkHttpUtils.CustomListener requestListener, boolean requestCompression) throws IOException { - return post(uri, requestBody, responseParser, requestListener, requestCompression, emptyMap()); - } - - @Override - public T post( - String uri, - RequestBody requestBody, - IOThrowingFunction responseParser, - @Nullable OkHttpUtils.CustomListener requestListener, - boolean requestCompression, - Map requestHeaders) - throws IOException { HttpUrl url = hostUrl.resolve(uri); Request.Builder requestBuilder = new Request.Builder() diff --git a/communication/src/test/java/datadog/communication/BackendApiFactoryTest.java b/communication/src/test/java/datadog/communication/BackendApiFactoryTest.java index 726c34a7f73..5368e4ad4ab 100644 --- a/communication/src/test/java/datadog/communication/BackendApiFactoryTest.java +++ b/communication/src/test/java/datadog/communication/BackendApiFactoryTest.java @@ -1,6 +1,7 @@ package datadog.communication; import static datadog.communication.ddagent.DDAgentFeaturesDiscovery.V4_EVP_PROXY_ENDPOINT; +import static java.util.Collections.singletonMap; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; @@ -45,7 +46,9 @@ void advertisedEvpProxyEndpointSupportsDisabledResponseCompression() throws Exce final FakeFeaturesDiscovery discovery = new FakeFeaturesDiscovery(V4_EVP_PROXY_ENDPOINT); final BackendApiFactory factory = new BackendApiFactory( - Config.get(), sharedCommunicationObjects(discovery, agent.url("/"))); + Config.get(), + sharedCommunicationObjects(discovery, agent.url("/")), + singletonMap("DD-EVP-ORIGIN", "dd-trace-java")); final BackendApi api = factory.createBackendApi(Intake.EVENT_PLATFORM, false); assertNotNull(api); @@ -58,6 +61,7 @@ void advertisedEvpProxyEndpointSupportsDisabledResponseCompression() throws Exce final RecordedRequest request = agent.takeRequest(); assertEquals("/evp_proxy/v4/api/v2/flagevaluation", request.getPath()); + assertEquals("dd-trace-java", request.getHeader("DD-EVP-ORIGIN")); } finally { agent.shutdown(); } diff --git a/communication/src/test/java/datadog/communication/BackendApiTest.java b/communication/src/test/java/datadog/communication/BackendApiTest.java deleted file mode 100644 index 225892e847b..00000000000 --- a/communication/src/test/java/datadog/communication/BackendApiTest.java +++ /dev/null @@ -1,45 +0,0 @@ -package datadog.communication; - -import static java.util.Collections.singletonMap; -import static org.junit.jupiter.api.Assertions.assertEquals; - -import datadog.communication.http.OkHttpUtils; -import datadog.communication.util.IOThrowingFunction; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import javax.annotation.Nullable; -import okhttp3.RequestBody; -import org.junit.jupiter.api.Test; - -class BackendApiTest { - - @Test - void defaultPostFallsBackToPostWithoutHeaders() throws IOException { - final BackendApi api = new TestBackendApi(); - - final String response = - api.post( - "flagevaluation", - null, - input -> "response", - null, - false, - singletonMap("DD-EVP-ORIGIN", "dd-trace-java")); - - assertEquals("response", response); - } - - private static final class TestBackendApi implements BackendApi { - @Override - public T post( - final String uri, - final RequestBody requestBody, - final IOThrowingFunction responseParser, - @Nullable final OkHttpUtils.CustomListener requestListener, - final boolean requestCompression) - throws IOException { - return responseParser.apply(new ByteArrayInputStream(new byte[0])); - } - } -} diff --git a/communication/src/test/java/datadog/communication/EvpProxyApiTest.java b/communication/src/test/java/datadog/communication/EvpProxyApiTest.java index 79adb0fa3cd..14c6962bf8e 100644 --- a/communication/src/test/java/datadog/communication/EvpProxyApiTest.java +++ b/communication/src/test/java/datadog/communication/EvpProxyApiTest.java @@ -1,13 +1,10 @@ package datadog.communication; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import datadog.communication.http.HttpRetryPolicy; import java.io.IOException; -import java.util.HashMap; -import java.util.Map; import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.RequestBody; @@ -64,39 +61,5 @@ void reportsHttpStatusForRejectedRequest() throws Exception { final RecordedRequest request = server.takeRequest(); assertEquals("/evp_proxy/v4/api/v2/exposures", request.getPath()); assertEquals("event-platform-intake", request.getHeader("X-Datadog-EVP-Subdomain")); - assertNull(request.getHeader("DD-EVP-ORIGIN")); - assertNull(request.getHeader("DD-EVP-ORIGIN-VERSION")); - } - - @Test - void addsCustomHeadersWithoutReplacingEvpHeaders() throws Exception { - server.enqueue(new MockResponse().setResponseCode(200)); - final EvpProxyApi api = - new EvpProxyApi( - "123", - server.url("/evp_proxy/v4/"), - "event-platform-intake", - HttpRetryPolicy.Factory.NEVER_RETRY, - client, - false); - final Map requestHeaders = new HashMap<>(); - requestHeaders.put("DD-EVP-ORIGIN", "dd-trace-java"); - requestHeaders.put("DD-EVP-ORIGIN-VERSION", "1.2.3"); - - api.post( - "flagevaluation", - RequestBody.create(MediaType.parse("application/json"), "{}"), - stream -> null, - null, - false, - requestHeaders); - - final RecordedRequest request = server.takeRequest(); - assertEquals("/evp_proxy/v4/api/v2/flagevaluation", request.getPath()); - assertEquals("event-platform-intake", request.getHeader("X-Datadog-EVP-Subdomain")); - assertEquals("123", request.getHeader("x-datadog-trace-id")); - assertEquals("123", request.getHeader("x-datadog-parent-id")); - assertEquals("dd-trace-java", request.getHeader("DD-EVP-ORIGIN")); - assertEquals("1.2.3", request.getHeader("DD-EVP-ORIGIN-VERSION")); } } diff --git a/communication/src/test/java/datadog/communication/IntakeApiTest.java b/communication/src/test/java/datadog/communication/IntakeApiTest.java index 0f2f1d552c7..f34a22a952c 100644 --- a/communication/src/test/java/datadog/communication/IntakeApiTest.java +++ b/communication/src/test/java/datadog/communication/IntakeApiTest.java @@ -1,11 +1,10 @@ package datadog.communication; +import static java.util.Collections.singletonMap; import static org.junit.jupiter.api.Assertions.assertEquals; import datadog.communication.http.HttpRetryPolicy; import java.io.IOException; -import java.util.HashMap; -import java.util.Map; import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.RequestBody; @@ -48,7 +47,7 @@ void requestsIdentityResponseEncodingWhenCompressionIsDisabled() throws Exceptio } @Test - void addsCustomHeadersWithoutReplacingIntakeHeaders() throws Exception { + void addsConfiguredRequestHeaders() throws Exception { server.enqueue(new MockResponse().setResponseCode(200).setBody("{}")); final IntakeApi api = new IntakeApi( @@ -57,26 +56,14 @@ void addsCustomHeadersWithoutReplacingIntakeHeaders() throws Exception { "123", HttpRetryPolicy.Factory.NEVER_RETRY, client, - false); - final Map requestHeaders = new HashMap<>(); - requestHeaders.put("DD-EVP-ORIGIN", "dd-trace-java"); - requestHeaders.put("DD-EVP-ORIGIN-VERSION", "1.2.3"); + false, + singletonMap("DD-EVP-ORIGIN", "dd-trace-java")); - api.post( - "flagevaluation", - RequestBody.create(JSON, "{}"), - responseBody -> null, - null, - false, - requestHeaders); + api.post("exposures", RequestBody.create(JSON, "{}"), responseBody -> null, null, false); final RecordedRequest request = server.takeRequest(); - assertEquals("/api/v2/flagevaluation", request.getPath()); - assertEquals("api-key", request.getHeader("dd-api-key")); - assertEquals("123", request.getHeader("x-datadog-trace-id")); - assertEquals("123", request.getHeader("x-datadog-parent-id")); assertEquals("dd-trace-java", request.getHeader("DD-EVP-ORIGIN")); - assertEquals("1.2.3", request.getHeader("DD-EVP-ORIGIN-VERSION")); + assertEquals("api-key", request.getHeader("dd-api-key")); } private String postAndReadAcceptEncoding(final boolean responseCompression) throws Exception { diff --git a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/AgentlessFeatureFlagBackendApi.java b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/AgentlessFeatureFlagBackendApi.java index 45e08a37594..769ebfd1dd1 100644 --- a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/AgentlessFeatureFlagBackendApi.java +++ b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/AgentlessFeatureFlagBackendApi.java @@ -1,7 +1,5 @@ package com.datadog.featureflag; -import static java.util.Collections.emptyMap; - import datadog.communication.BackendApi; import datadog.communication.HttpResponseException; import datadog.communication.http.OkHttpUtils; @@ -9,7 +7,6 @@ import java.io.IOException; import java.io.InputStream; import java.net.ConnectException; -import java.util.Map; import java.util.function.Supplier; import javax.annotation.Nullable; import okhttp3.RequestBody; @@ -46,22 +43,10 @@ public T post( @Nullable final OkHttpUtils.CustomListener requestListener, final boolean requestCompression) throws IOException { - return post(uri, requestBody, responseParser, requestListener, requestCompression, emptyMap()); - } - - @Override - public T post( - final String uri, - final RequestBody requestBody, - final IOThrowingFunction responseParser, - @Nullable final OkHttpUtils.CustomListener requestListener, - final boolean requestCompression, - final Map requestHeaders) - throws IOException { final BackendApi selectedApi = activeApi; try { return selectedApi.post( - uri, requestBody, responseParser, requestListener, requestCompression, requestHeaders); + uri, requestBody, responseParser, requestListener, requestCompression); } catch (final IOException exception) { if (selectedApi != proxyApi || !isDefinitiveRejection(exception)) { throw exception; @@ -71,8 +56,7 @@ public T post( if (directApi == null) { throw exception; } - return directApi.post( - uri, requestBody, responseParser, requestListener, requestCompression, requestHeaders); + return directApi.post(uri, requestBody, responseParser, requestListener, requestCompression); } } diff --git a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagBackendApiFactory.java b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagBackendApiFactory.java index 0dbf9c74254..8d5314d911c 100644 --- a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagBackendApiFactory.java +++ b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagBackendApiFactory.java @@ -1,13 +1,17 @@ package com.datadog.featureflag; import static datadog.trace.api.featureflag.config.FeatureFlaggingConfig.CONFIGURATION_SOURCE_AGENTLESS; +import static java.util.Collections.unmodifiableMap; import datadog.communication.BackendApi; import datadog.communication.BackendApiFactory; import datadog.communication.ddagent.SharedCommunicationObjects; +import datadog.communication.ddagent.TracerVersion; import datadog.communication.http.HttpRetryPolicy; import datadog.trace.api.Config; import datadog.trace.api.intake.Intake; +import java.util.HashMap; +import java.util.Map; import javax.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -16,6 +20,7 @@ final class FeatureFlagBackendApiFactory { private static final Logger LOGGER = LoggerFactory.getLogger(FeatureFlagBackendApiFactory.class); + static final Map REQUEST_HEADERS = requestHeaders(); private final Config config; private final BackendApiFactory backendApiFactory; @@ -25,7 +30,10 @@ final class FeatureFlagBackendApiFactory { final Config config, final SharedCommunicationObjects sharedCommunicationObjects, final FeatureFlagEventType eventType) { - this(config, new BackendApiFactory(config, sharedCommunicationObjects), eventType); + this( + config, + new BackendApiFactory(config, sharedCommunicationObjects, REQUEST_HEADERS), + eventType); } FeatureFlagBackendApiFactory( @@ -78,6 +86,13 @@ BackendApi create() { return null; } + private static Map requestHeaders() { + final Map headers = new HashMap<>(2); + headers.put("DD-EVP-ORIGIN", "dd-trace-java"); + headers.put("DD-EVP-ORIGIN-VERSION", TracerVersion.TRACER_VERSION); + return unmodifiableMap(headers); + } + private boolean hasDirectCredentials() { final String apiKey = config.getApiKey(); return apiKey != null && !apiKey.isEmpty(); diff --git a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagEvpPublisher.java b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagEvpPublisher.java index 9543317568f..8a024b52f43 100644 --- a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagEvpPublisher.java +++ b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagEvpPublisher.java @@ -1,18 +1,12 @@ package com.datadog.featureflag; -import static java.util.Collections.emptyMap; -import static java.util.Collections.unmodifiableMap; - import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.Moshi; import datadog.communication.BackendApi; import datadog.communication.BackendApiFactory; -import datadog.communication.ddagent.TracerVersion; import datadog.trace.api.intake.Intake; import java.io.IOException; import java.io.UnsupportedEncodingException; -import java.util.HashMap; -import java.util.Map; import java.util.function.Supplier; import okhttp3.MediaType; import okhttp3.RequestBody; @@ -20,8 +14,6 @@ final class FeatureFlagEvpPublisher { private static final MediaType JSON = MediaType.parse("application/json"); - private static final String FLAG_EVALUATION_ROUTE = "flagevaluation"; - private static final Map FLAG_EVALUATION_HEADERS = flagEvaluationHeaders(); private final Supplier backendApiSupplier; private final JsonAdapter jsonAdapter; @@ -66,18 +58,7 @@ void post(final String route, final byte[] json) throws IOException { throw new IllegalStateException("EVP Proxy not available"); } final RequestBody requestBody = RequestBody.create(JSON, json); - evp.post(route, requestBody, stream -> null, null, false, requestHeaders(route)); - } - - private static Map requestHeaders(final String route) { - return FLAG_EVALUATION_ROUTE.equals(route) ? FLAG_EVALUATION_HEADERS : emptyMap(); - } - - private static Map flagEvaluationHeaders() { - final Map headers = new HashMap<>(2); - headers.put("DD-EVP-ORIGIN", "dd-trace-java"); - headers.put("DD-EVP-ORIGIN-VERSION", TracerVersion.TRACER_VERSION); - return unmodifiableMap(headers); + evp.post(route, requestBody, stream -> null, null, false); } static byte[] utf8Bytes(final String json) { diff --git a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/AgentlessFeatureFlagBackendApiTest.java b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/AgentlessFeatureFlagBackendApiTest.java index 6d57b3e5cba..117c72ba2a1 100644 --- a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/AgentlessFeatureFlagBackendApiTest.java +++ b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/AgentlessFeatureFlagBackendApiTest.java @@ -1,7 +1,5 @@ package com.datadog.featureflag; -import static java.util.Collections.emptyMap; -import static java.util.Collections.singletonMap; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -17,7 +15,6 @@ import java.net.SocketTimeoutException; import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Stream; import javax.annotation.Nullable; @@ -61,22 +58,6 @@ void replaysRejectedBatchDirectlyAndKeepsDirectRoute(final int statusCode) throw assertSame(secondBody, direct.requestBodies.get(1)); } - @Test - void preservesRequestHeadersWhenReplayingRejectedBatch() throws Exception { - final RecordingBackendApi local = - new RecordingBackendApi(new HttpResponseException(404, "rejected")); - final RecordingBackendApi direct = new RecordingBackendApi(); - final AgentlessFeatureFlagBackendApi api = - new AgentlessFeatureFlagBackendApi(local, () -> direct, "flag evaluation"); - final Map requestHeaders = singletonMap("DD-EVP-ORIGIN", "dd-trace-java"); - - api.post( - "flagevaluation", requestBody("evaluation"), stream -> null, null, false, requestHeaders); - - assertSame(requestHeaders, local.requestHeaders.get(0)); - assertSame(requestHeaders, direct.requestHeaders.get(0)); - } - @ParameterizedTest @MethodSource("featureFlagRoutes") void fallsBackAfterConnectionRefusal(final String route, final String eventType) @@ -186,7 +167,6 @@ private static Stream featureFlagRoutes() { private static final class RecordingBackendApi implements BackendApi { private IOException failure; private final List requestBodies = new ArrayList<>(); - private final List> requestHeaders = new ArrayList<>(); private int calls; private RecordingBackendApi() { @@ -205,26 +185,8 @@ public T post( @Nullable final OkHttpUtils.CustomListener requestListener, final boolean requestCompression) throws IOException { - return record(requestBody, emptyMap()); - } - - @Override - public T post( - final String uri, - final RequestBody requestBody, - final IOThrowingFunction responseParser, - @Nullable final OkHttpUtils.CustomListener requestListener, - final boolean requestCompression, - final Map requestHeaders) - throws IOException { - return record(requestBody, requestHeaders); - } - - private T record(final RequestBody requestBody, final Map requestHeaders) - throws IOException { calls++; requestBodies.add(requestBody); - this.requestHeaders.add(requestHeaders); if (failure != null) { throw failure; } diff --git a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/ExposureWriterTests.java b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/ExposureWriterTests.java index 0c22b61bcf8..daaf213ebfb 100644 --- a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/ExposureWriterTests.java +++ b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/ExposureWriterTests.java @@ -10,7 +10,6 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyMap; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -314,7 +313,7 @@ void testAmbiguousExposureBatchIsNotRetriedOrReplayedDirectly() throws Exception .thenReturn(proxyApi); when(backendApiFactory.createDirectIntakeApi(Intake.EVENT_PLATFORM, true)) .thenReturn(directApi); - when(proxyApi.post(eq("exposures"), any(RequestBody.class), any(), any(), eq(false), anyMap())) + when(proxyApi.post(eq("exposures"), any(RequestBody.class), any(), any(), eq(false))) .thenThrow(new SocketTimeoutException("ambiguous timeout")) .thenThrow(new ConnectException("definitive refusal")); final FeatureFlagBackendApiFactory featureFlagBackendApiFactory = @@ -329,24 +328,21 @@ void testAmbiguousExposureBatchIsNotRetriedOrReplayedDirectly() throws Exception poll.eventually( () -> verify(proxyApi) - .post( - eq("exposures"), any(RequestBody.class), any(), any(), eq(false), anyMap())); + .post(eq("exposures"), any(RequestBody.class), any(), any(), eq(false))); MILLISECONDS.sleep(300); verify(proxyApi, times(1)) - .post(eq("exposures"), any(RequestBody.class), any(), any(), eq(false), anyMap()); + .post(eq("exposures"), any(RequestBody.class), any(), any(), eq(false)); verify(directApi, never()) - .post(eq("exposures"), any(RequestBody.class), any(), any(), eq(false), anyMap()); + .post(eq("exposures"), any(RequestBody.class), any(), any(), eq(false)); writer.accept(exposures.get(1)); poll.eventually( () -> verify(directApi) - .post( - eq("exposures"), any(RequestBody.class), any(), any(), eq(false), anyMap())); + .post(eq("exposures"), any(RequestBody.class), any(), any(), eq(false))); final ArgumentCaptor directBody = ArgumentCaptor.forClass(RequestBody.class); - verify(directApi) - .post(eq("exposures"), directBody.capture(), any(), any(), eq(false), anyMap()); + verify(directApi).post(eq("exposures"), directBody.capture(), any(), any(), eq(false)); final Buffer buffer = new Buffer(); directBody.getValue().writeTo(buffer); final ExposuresRequest directRequest = diff --git a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FeatureFlagBackendApiFactoryTest.java b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FeatureFlagBackendApiFactoryTest.java index 8b117742715..3544e66b02c 100644 --- a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FeatureFlagBackendApiFactoryTest.java +++ b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FeatureFlagBackendApiFactoryTest.java @@ -4,6 +4,7 @@ import static com.datadog.featureflag.FeatureFlagEventType.FLAG_EVALUATION; import static datadog.trace.api.featureflag.config.FeatureFlaggingConfig.CONFIGURATION_SOURCE_AGENTLESS; import static datadog.trace.api.featureflag.config.FeatureFlaggingConfig.CONFIGURATION_SOURCE_REMOTE_CONFIG; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; @@ -14,6 +15,7 @@ import datadog.communication.BackendApi; import datadog.communication.BackendApiFactory; +import datadog.communication.ddagent.TracerVersion; import datadog.communication.http.HttpRetryPolicy; import datadog.trace.api.Config; import datadog.trace.api.intake.Intake; @@ -21,6 +23,15 @@ class FeatureFlagBackendApiFactoryTest { + @Test + void configuresSdkIdentityHeadersForAllFeatureFlagEventTypes() { + assertEquals( + "dd-trace-java", FeatureFlagBackendApiFactory.REQUEST_HEADERS.get("DD-EVP-ORIGIN")); + assertEquals( + TracerVersion.TRACER_VERSION, + FeatureFlagBackendApiFactory.REQUEST_HEADERS.get("DD-EVP-ORIGIN-VERSION")); + } + @Test void remoteConfigUsesOnlyLocalEvpProxy() { final Config config = config(CONFIGURATION_SOURCE_REMOTE_CONFIG, "api-key"); diff --git a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FeatureFlagEvpPublisherTest.java b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FeatureFlagEvpPublisherTest.java index 4d94a982ad8..379dd49e444 100644 --- a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FeatureFlagEvpPublisherTest.java +++ b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FeatureFlagEvpPublisherTest.java @@ -1,6 +1,5 @@ package com.datadog.featureflag; -import static java.util.Collections.emptyMap; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; @@ -13,10 +12,7 @@ import datadog.communication.BackendApi; import datadog.communication.BackendApiFactory; -import datadog.communication.ddagent.TracerVersion; import datadog.trace.api.intake.Intake; -import java.util.HashMap; -import java.util.Map; import okhttp3.RequestBody; import org.junit.jupiter.api.Test; @@ -50,25 +46,7 @@ void responseCompressionCanBeDisabled() throws Exception { verify(factory).createBackendApi(Intake.EVENT_PLATFORM, false); verify(backendApi) - .post( - eq("flagevaluation"), - any(RequestBody.class), - any(), - isNull(), - eq(false), - eq(flagEvaluationHeaders())); - } - - @Test - void exposureRequestsDoNotIncludeFlagEvaluationHeaders() throws Exception { - final BackendApi backendApi = mock(BackendApi.class); - final FeatureFlagEvpPublisher publisher = - new FeatureFlagEvpPublisher<>(() -> backendApi, TestRequest.class); - - publisher.post("exposures", new TestRequest("value")); - - verify(backendApi) - .post(eq("exposures"), any(RequestBody.class), any(), isNull(), eq(false), eq(emptyMap())); + .post(eq("flagevaluation"), any(RequestBody.class), any(), isNull(), eq(false)); } @Test @@ -83,13 +61,6 @@ void postThrowsWhenEvpBackendApiCannotBeCreated() { () -> publisher.post("flagevaluation", FeatureFlagEvpPublisher.utf8Bytes("{}"))); } - private static Map flagEvaluationHeaders() { - final Map headers = new HashMap<>(); - headers.put("DD-EVP-ORIGIN", "dd-trace-java"); - headers.put("DD-EVP-ORIGIN-VERSION", TracerVersion.TRACER_VERSION); - return headers; - } - static class TestRequest { public final String value; diff --git a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FlagEvaluationPayloadsTest.java b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FlagEvaluationPayloadsTest.java index 9c5ed201e0c..d4ca517fffd 100644 --- a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FlagEvaluationPayloadsTest.java +++ b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FlagEvaluationPayloadsTest.java @@ -52,7 +52,6 @@ void fullTierPayloadUsesWorkerWireShape() throws Exception { assertNotNull(evalAttrs); assertEquals("us-east-1", evalAttrs.get("region")); assertFalse(ev.containsKey("reason")); - assertFalse(ev.containsKey("source")); } @Test @@ -97,7 +96,6 @@ void degradedTierEventOmitsTargetingKeyAndContext() throws Exception { final Map ev = firstEvent(json); assertNull(ev.get("targeting_key")); assertNull(ev.get("context")); - assertFalse(ev.containsKey("source")); } @Test diff --git a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FlagEvaluationTestSupport.java b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FlagEvaluationTestSupport.java index b1f94fc0e3e..502f0e76c69 100644 --- a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FlagEvaluationTestSupport.java +++ b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FlagEvaluationTestSupport.java @@ -6,7 +6,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyMap; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -133,8 +132,7 @@ static CapturedJson flushAndCapture(final TestWriterSetup setup) throws Exceptio static List flushAndCaptureAll(final TestWriterSetup setup) throws Exception { final List captured = new ArrayList<>(); - when(setup.mockEvp.post( - eq("flagevaluation"), any(RequestBody.class), any(), any(), eq(false), anyMap())) + when(setup.mockEvp.post(eq("flagevaluation"), any(RequestBody.class), any(), any(), eq(false))) .thenAnswer( inv -> { captured.add(inv.getArgument(1)); diff --git a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FlagEvaluationWriterImplTest.java b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FlagEvaluationWriterImplTest.java index da19e448dd2..ba3d03ec6d5 100644 --- a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FlagEvaluationWriterImplTest.java +++ b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FlagEvaluationWriterImplTest.java @@ -22,7 +22,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyMap; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doAnswer; @@ -378,8 +377,7 @@ void finalFlushRunsWithoutTheInterruptFlagSet() throws Exception { final java.util.concurrent.CountDownLatch posted = new java.util.concurrent.CountDownLatch(1); final boolean[] interruptedDuringPost = {true}; final BackendApi mockEvp = mock(BackendApi.class); - when(mockEvp.post( - eq("flagevaluation"), any(RequestBody.class), any(), any(), eq(false), anyMap())) + when(mockEvp.post(eq("flagevaluation"), any(RequestBody.class), any(), any(), eq(false))) .thenAnswer( inv -> { interruptedDuringPost[0] = Thread.currentThread().isInterrupted(); @@ -408,8 +406,7 @@ void closeDrainsAndFinalFlushesQueuedEvents() throws Exception { final java.util.concurrent.CountDownLatch posted = new java.util.concurrent.CountDownLatch(1); final RequestBody[] captured = {null}; final BackendApi mockEvp = mock(BackendApi.class); - when(mockEvp.post( - eq("flagevaluation"), any(RequestBody.class), any(), any(), eq(false), anyMap())) + when(mockEvp.post(eq("flagevaluation"), any(RequestBody.class), any(), any(), eq(false))) .thenAnswer( inv -> { captured[0] = inv.getArgument(1); @@ -451,8 +448,7 @@ void continuousTrafficFlushesWithoutWaitingForIdle() throws Exception { writer.enqueue(simpleEvent("busy-flag", "on")); try { verify(mockEvp, atLeastOnce()) - .post( - eq("flagevaluation"), any(RequestBody.class), any(), any(), eq(false), anyMap()); + .post(eq("flagevaluation"), any(RequestBody.class), any(), any(), eq(false)); posted = true; break; } catch (AssertionError ignored) { @@ -476,8 +472,7 @@ void flushPostsToFlagevaluationEndpoint() throws Exception { setup.handler.flush(); verify(setup.factory).createBackendApi(Intake.EVENT_PLATFORM, false); - verify(mockEvp) - .post(eq("flagevaluation"), any(RequestBody.class), any(), any(), eq(false), anyMap()); + verify(mockEvp).post(eq("flagevaluation"), any(RequestBody.class), any(), any(), eq(false)); } @Test @@ -494,7 +489,7 @@ void splitPostFailureDoesNotRetryAlreadySentPayloads() throws Exception { return null; }) .when(mockEvp) - .post(eq("flagevaluation"), any(RequestBody.class), any(), any(), eq(false), anyMap()); + .post(eq("flagevaluation"), any(RequestBody.class), any(), any(), eq(false)); for (int i = 0; i < 4; i++) { final Map attrs = new HashMap<>(); @@ -560,8 +555,7 @@ void eventConsentFalseStaysHashedEvenWhenGatewayLaterReportsTrue() throws Except setup.handler.drainAndAggregate(); final java.util.List captured = new java.util.ArrayList<>(); - when(mockEvp.post( - eq("flagevaluation"), any(RequestBody.class), any(), any(), eq(false), anyMap())) + when(mockEvp.post(eq("flagevaluation"), any(RequestBody.class), any(), any(), eq(false))) .thenAnswer( inv -> { captured.add(inv.getArgument(1)); @@ -594,8 +588,7 @@ void eventConsentTrueStaysRawEvenWhenGatewayLaterReportsFalse() throws Exception setup.handler.drainAndAggregate(); final java.util.List captured = new java.util.ArrayList<>(); - when(mockEvp.post( - eq("flagevaluation"), any(RequestBody.class), any(), any(), eq(false), anyMap())) + when(mockEvp.post(eq("flagevaluation"), any(RequestBody.class), any(), any(), eq(false))) .thenAnswer( inv -> { captured.add(inv.getArgument(1)); @@ -661,14 +654,13 @@ void encodeFailureClearsAggregatorSoLaterFlushesRecover() throws Exception { setup.handler.drainAndAggregate(); setup.handler.flush(); verify(mockEvp, org.mockito.Mockito.never()) - .post(eq("flagevaluation"), any(RequestBody.class), any(), any(), eq(false), anyMap()); + .post(eq("flagevaluation"), any(RequestBody.class), any(), any(), eq(false)); // The bucket must not survive the failed flush. A follow-up healthy event flushes cleanly. setup.handler.add(simpleEvent("healthy-flag", "on")); setup.handler.drainAndAggregate(); setup.handler.flush(); - verify(mockEvp) - .post(eq("flagevaluation"), any(RequestBody.class), any(), any(), eq(false), anyMap()); + verify(mockEvp).post(eq("flagevaluation"), any(RequestBody.class), any(), any(), eq(false)); } @Test From 1cb2c70e1936c02d0a36f4bd09980914df8b484f Mon Sep 17 00:00:00 2001 From: Vickie Boettcher Date: Thu, 27 Aug 2026 21:58:21 -0400 Subject: [PATCH 8/8] refactor(openfeature): inject SDK headers with HTTP client Scope the SDK identity headers to feature-flag HTTP clients so both evaluation and exposure requests retain them across proxy and direct intake without modifying transport implementations.\n\nGenerated with Claude Code --- .../communication/BackendApiFactory.java | 29 +++++++++++++++---- .../datadog/communication/EvpProxyApi.java | 29 ------------------- .../java/datadog/communication/IntakeApi.java | 22 -------------- .../communication/BackendApiFactoryTest.java | 2 ++ .../datadog/communication/IntakeApiTest.java | 21 -------------- 5 files changed, 25 insertions(+), 78 deletions(-) diff --git a/communication/src/main/java/datadog/communication/BackendApiFactory.java b/communication/src/main/java/datadog/communication/BackendApiFactory.java index 347b5d4c42d..4ea35cb8596 100644 --- a/communication/src/main/java/datadog/communication/BackendApiFactory.java +++ b/communication/src/main/java/datadog/communication/BackendApiFactory.java @@ -13,6 +13,8 @@ import java.util.Map; import javax.annotation.Nullable; import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.Request; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -74,9 +76,8 @@ public BackendApi createDirectIntakeApi(Intake intake, boolean responseCompressi apiKey, traceId, retryPolicyFactory(), - sharedCommunicationObjects.getIntakeHttpClient(), - responseCompression, - requestHeaders); + withRequestHeaders(sharedCommunicationObjects.getIntakeHttpClient()), + responseCompression); } /** Creates an API client that uses the specified retry policy with a compatible local proxy. */ @@ -113,9 +114,25 @@ public BackendApi createDirectIntakeApi(Intake intake, boolean responseCompressi evpProxyUrl, subdomain, retryPolicyFactory, - sharedCommunicationObjects.agentHttpClient, - responseCompression, - requestHeaders); + withRequestHeaders(sharedCommunicationObjects.agentHttpClient), + responseCompression); + } + + private OkHttpClient withRequestHeaders(final OkHttpClient httpClient) { + if (requestHeaders.isEmpty()) { + return httpClient; + } + return httpClient + .newBuilder() + .addInterceptor( + chain -> { + final Request.Builder requestBuilder = chain.request().newBuilder(); + for (Map.Entry header : requestHeaders.entrySet()) { + requestBuilder.header(header.getKey(), header.getValue()); + } + return chain.proceed(requestBuilder.build()); + }) + .build(); } private static HttpRetryPolicy.Factory retryPolicyFactory() { diff --git a/communication/src/main/java/datadog/communication/EvpProxyApi.java b/communication/src/main/java/datadog/communication/EvpProxyApi.java index 48adf199739..8bb768b7e4e 100644 --- a/communication/src/main/java/datadog/communication/EvpProxyApi.java +++ b/communication/src/main/java/datadog/communication/EvpProxyApi.java @@ -1,15 +1,10 @@ package datadog.communication; -import static java.util.Collections.emptyMap; -import static java.util.Collections.unmodifiableMap; - import datadog.communication.http.HttpRetryPolicy; import datadog.communication.http.OkHttpUtils; import datadog.communication.util.IOThrowingFunction; import java.io.IOException; import java.io.InputStream; -import java.util.HashMap; -import java.util.Map; import java.util.zip.GZIPInputStream; import javax.annotation.Nullable; import okhttp3.HttpUrl; @@ -38,7 +33,6 @@ public class EvpProxyApi implements BackendApi { private final String subdomain; private final OkHttpClient httpClient; private final boolean responseCompression; - private final Map requestHeaders; public EvpProxyApi( String traceId, @@ -47,31 +41,12 @@ public EvpProxyApi( HttpRetryPolicy.Factory retryPolicyFactory, OkHttpClient httpClient, boolean responseCompression) { - this( - traceId, - evpProxyUrl, - subdomain, - retryPolicyFactory, - httpClient, - responseCompression, - emptyMap()); - } - - public EvpProxyApi( - String traceId, - HttpUrl evpProxyUrl, - String subdomain, - HttpRetryPolicy.Factory retryPolicyFactory, - OkHttpClient httpClient, - boolean responseCompression, - Map requestHeaders) { this.traceId = traceId; this.evpProxyUrl = evpProxyUrl.resolve("api/" + API_VERSION + "/"); this.subdomain = subdomain; this.retryPolicyFactory = retryPolicyFactory; this.httpClient = httpClient; this.responseCompression = responseCompression; - this.requestHeaders = unmodifiableMap(new HashMap<>(requestHeaders)); } @Override @@ -91,10 +66,6 @@ public T post( .addHeader(X_DATADOG_TRACE_ID_HEADER, traceId) .addHeader(X_DATADOG_PARENT_ID_HEADER, traceId); - for (Map.Entry header : requestHeaders.entrySet()) { - requestBuilder.addHeader(header.getKey(), header.getValue()); - } - if (requestListener != null) { requestBuilder.tag(OkHttpUtils.CustomListener.class, requestListener); } diff --git a/communication/src/main/java/datadog/communication/IntakeApi.java b/communication/src/main/java/datadog/communication/IntakeApi.java index fa431a29786..1a6f3f91bc7 100644 --- a/communication/src/main/java/datadog/communication/IntakeApi.java +++ b/communication/src/main/java/datadog/communication/IntakeApi.java @@ -1,15 +1,10 @@ package datadog.communication; -import static java.util.Collections.emptyMap; -import static java.util.Collections.unmodifiableMap; - import datadog.communication.http.HttpRetryPolicy; import datadog.communication.http.OkHttpUtils; import datadog.communication.util.IOThrowingFunction; import java.io.IOException; import java.io.InputStream; -import java.util.HashMap; -import java.util.Map; import java.util.zip.GZIPInputStream; import javax.annotation.Nullable; import okhttp3.HttpUrl; @@ -38,7 +33,6 @@ public class IntakeApi implements BackendApi { private final boolean responseCompression; private final HttpUrl hostUrl; private final OkHttpClient httpClient; - private final Map requestHeaders; public IntakeApi( HttpUrl hostUrl, @@ -47,24 +41,12 @@ public IntakeApi( HttpRetryPolicy.Factory retryPolicyFactory, OkHttpClient httpClient, boolean responseCompression) { - this(hostUrl, apiKey, traceId, retryPolicyFactory, httpClient, responseCompression, emptyMap()); - } - - public IntakeApi( - HttpUrl hostUrl, - String apiKey, - String traceId, - HttpRetryPolicy.Factory retryPolicyFactory, - OkHttpClient httpClient, - boolean responseCompression, - Map requestHeaders) { this.hostUrl = hostUrl; this.apiKey = apiKey; this.traceId = traceId; this.retryPolicyFactory = retryPolicyFactory; this.responseCompression = responseCompression; this.httpClient = httpClient; - this.requestHeaders = unmodifiableMap(new HashMap<>(requestHeaders)); } @Override @@ -84,10 +66,6 @@ public T post( .addHeader(X_DATADOG_TRACE_ID_HEADER, traceId) .addHeader(X_DATADOG_PARENT_ID_HEADER, traceId); - for (Map.Entry header : requestHeaders.entrySet()) { - requestBuilder.addHeader(header.getKey(), header.getValue()); - } - if (requestListener != null) { requestBuilder.tag(OkHttpUtils.CustomListener.class, requestListener); } diff --git a/communication/src/test/java/datadog/communication/BackendApiFactoryTest.java b/communication/src/test/java/datadog/communication/BackendApiFactoryTest.java index 5368e4ad4ab..d81000d43d2 100644 --- a/communication/src/test/java/datadog/communication/BackendApiFactoryTest.java +++ b/communication/src/test/java/datadog/communication/BackendApiFactoryTest.java @@ -61,7 +61,9 @@ void advertisedEvpProxyEndpointSupportsDisabledResponseCompression() throws Exce final RecordedRequest request = agent.takeRequest(); assertEquals("/evp_proxy/v4/api/v2/flagevaluation", request.getPath()); + assertEquals("event-platform-intake", request.getHeader("X-Datadog-EVP-Subdomain")); assertEquals("dd-trace-java", request.getHeader("DD-EVP-ORIGIN")); + assertEquals("identity", request.getHeader("Accept-Encoding")); } finally { agent.shutdown(); } diff --git a/communication/src/test/java/datadog/communication/IntakeApiTest.java b/communication/src/test/java/datadog/communication/IntakeApiTest.java index f34a22a952c..326cf21ca84 100644 --- a/communication/src/test/java/datadog/communication/IntakeApiTest.java +++ b/communication/src/test/java/datadog/communication/IntakeApiTest.java @@ -1,6 +1,5 @@ package datadog.communication; -import static java.util.Collections.singletonMap; import static org.junit.jupiter.api.Assertions.assertEquals; import datadog.communication.http.HttpRetryPolicy; @@ -46,26 +45,6 @@ void requestsIdentityResponseEncodingWhenCompressionIsDisabled() throws Exceptio assertEquals("identity", postAndReadAcceptEncoding(false)); } - @Test - void addsConfiguredRequestHeaders() throws Exception { - server.enqueue(new MockResponse().setResponseCode(200).setBody("{}")); - final IntakeApi api = - new IntakeApi( - server.url("/api/v2/"), - "api-key", - "123", - HttpRetryPolicy.Factory.NEVER_RETRY, - client, - false, - singletonMap("DD-EVP-ORIGIN", "dd-trace-java")); - - api.post("exposures", RequestBody.create(JSON, "{}"), responseBody -> null, null, false); - - final RecordedRequest request = server.takeRequest(); - assertEquals("dd-trace-java", request.getHeader("DD-EVP-ORIGIN")); - assertEquals("api-key", request.getHeader("dd-api-key")); - } - private String postAndReadAcceptEncoding(final boolean responseCompression) throws Exception { server.enqueue(new MockResponse().setResponseCode(200).setBody("{}")); final IntakeApi api =