Repository navigation
Send Java SDK identity with Feature Flagging EVP events #12575
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
96a3427
1ac2d95
923a274
ceb8629
8ad35c4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,16 +1,21 @@ | ||
| package datadog.communication; | ||
|
|
||
| import static java.util.Collections.emptyList; | ||
| 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 okhttp3.OkHttpClient; | ||
| import okhttp3.Request; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
|
|
@@ -22,24 +27,41 @@ public class BackendApiFactory { | |
|
|
||
| private final Config config; | ||
| private final SharedCommunicationObjects sharedCommunicationObjects; | ||
| private final Map<String, String> requestHeaders; | ||
| private final boolean sendOnce; | ||
|
|
||
| public BackendApiFactory(Config config, SharedCommunicationObjects sharedCommunicationObjects) { | ||
| this(config, sharedCommunicationObjects, false); | ||
| this(config, sharedCommunicationObjects, emptyMap()); | ||
| } | ||
|
|
||
| public BackendApiFactory( | ||
| Config config, | ||
| SharedCommunicationObjects sharedCommunicationObjects, | ||
| Map<String, String> requestHeaders) { | ||
| this(config, sharedCommunicationObjects, requestHeaders, false); | ||
| } | ||
|
|
||
| public BackendApiFactory( | ||
| Config config, SharedCommunicationObjects sharedCommunicationObjects, boolean sendOnce) { | ||
| this(config, sharedCommunicationObjects, emptyMap(), sendOnce); | ||
| } | ||
|
|
||
| /** | ||
| * Creates a backend factory with optional send-once transport semantics. | ||
| * Creates a backend factory with per-request headers and optional send-once transport semantics. | ||
| * | ||
| * <p>When {@code sendOnce} is true, automatic connection retries and redirects are disabled, and | ||
| * the default HTTP retry policy is {@link HttpRetryPolicy.Factory#NEVER_RETRY}. Explicit caller | ||
| * policies may retry proven pre-send failures, but must not replay an ambiguously accepted event | ||
| * payload without an idempotency key. | ||
| */ | ||
| public BackendApiFactory( | ||
| Config config, SharedCommunicationObjects sharedCommunicationObjects, boolean sendOnce) { | ||
| Config config, | ||
| SharedCommunicationObjects sharedCommunicationObjects, | ||
| Map<String, String> requestHeaders, | ||
| boolean sendOnce) { | ||
| this.config = config; | ||
| this.sharedCommunicationObjects = sharedCommunicationObjects; | ||
| this.requestHeaders = unmodifiableMap(new HashMap<>(requestHeaders)); | ||
| this.sendOnce = sendOnce; | ||
| } | ||
|
|
||
|
|
@@ -182,7 +204,7 @@ private static boolean isAsciiLetterOrDigit(final char character) { | |
| * Creates an EVP proxy client after Agent discovery, optionally forcing a fresh discovery and | ||
| * requiring the Agent to advertise every specified forwarding header. | ||
| * | ||
| * <p>Callers supply the forwarding capabilities their event protocol requires. | ||
| * <p>Required forwarding capabilities are independent of any headers configured on this factory. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ⚪ Nit — documentation preference, strictly optional The replaced sentence said who decides what to pass. The replacement says what does not feed the gate. Both are useful; one line holds both.
The new sentence answers the question this PR creates — do the configured request headers feed the gate? They do not, and that is worth stating. To be fair to the edit, the retained summary at line 204 already describes the mechanism ("requiring the Agent to advertise every specified forwarding header"), so nothing essential was lost. What went is the hint that the caller's event protocol is what determines the list. Suggested change: /**
* Creates an EVP proxy client after Agent discovery, optionally forcing a fresh discovery and
* requiring the Agent to advertise every specified forwarding header.
*
* <p>Callers supply the forwarding capabilities their event protocol requires. These are
* independent of any headers configured on this factory.
*
* <p>The {@code forceDiscovery} form is intended for bounded unavailable-route recovery probes.
*/ |
||
| * | ||
| * <p>The {@code forceDiscovery} form is intended for bounded unavailable-route recovery probes. | ||
| */ | ||
|
|
@@ -250,14 +272,24 @@ private BackendApi createEvpProxyApi( | |
| } | ||
|
|
||
| OkHttpClient configureHttpClient(final OkHttpClient httpClient) { | ||
| return sendOnce | ||
| ? httpClient | ||
| .newBuilder() | ||
| .retryOnConnectionFailure(false) | ||
| .followRedirects(false) | ||
| .followSslRedirects(false) | ||
| .build() | ||
| : httpClient; | ||
| if (requestHeaders.isEmpty() && !sendOnce) { | ||
| return httpClient; | ||
| } | ||
| final OkHttpClient.Builder builder = httpClient.newBuilder(); | ||
| if (sendOnce) { | ||
| builder.retryOnConnectionFailure(false).followRedirects(false).followSslRedirects(false); | ||
| } | ||
| if (!requestHeaders.isEmpty()) { | ||
| builder.addInterceptor( | ||
| chain -> { | ||
| final Request.Builder requestBuilder = chain.request().newBuilder(); | ||
| for (Map.Entry<String, String> header : requestHeaders.entrySet()) { | ||
| requestBuilder.header(header.getKey(), header.getValue()); | ||
| } | ||
| return chain.proceed(requestBuilder.build()); | ||
| }); | ||
| } | ||
| return builder.build(); | ||
| } | ||
|
|
||
| private HttpRetryPolicy.Factory retryPolicyFactory() { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,14 @@ | ||
| package datadog.communication; | ||
|
|
||
| import static datadog.communication.EvpProxy.JAVA_TRACING_LIBRARY; | ||
| import static datadog.communication.EvpProxy.ORIGIN_HEADER; | ||
| import static datadog.communication.EvpProxy.ORIGIN_VERSION_HEADER; | ||
| import static datadog.communication.ddagent.DDAgentFeaturesDiscovery.V2_EVP_PROXY_ENDPOINT; | ||
| import static datadog.communication.ddagent.DDAgentFeaturesDiscovery.V4_EVP_PROXY_ENDPOINT; | ||
| import static datadog.trace.api.config.CiVisibilityConfig.CIVISIBILITY_AGENTLESS_URL; | ||
| import static datadog.trace.api.config.GeneralConfig.API_KEY; | ||
| import static java.util.Arrays.asList; | ||
| import static java.util.Collections.singletonMap; | ||
| import static org.junit.jupiter.api.Assertions.assertEquals; | ||
| import static org.junit.jupiter.api.Assertions.assertFalse; | ||
| import static org.junit.jupiter.api.Assertions.assertNotNull; | ||
|
|
@@ -22,7 +26,10 @@ | |
| import java.io.IOException; | ||
| import java.nio.charset.StandardCharsets; | ||
| import java.util.Arrays; | ||
| import java.util.HashMap; | ||
| import java.util.Locale; | ||
| import java.util.Map; | ||
| import java.util.Properties; | ||
| import java.util.stream.Stream; | ||
| import okhttp3.HttpUrl; | ||
| import okhttp3.MediaType; | ||
|
|
@@ -327,15 +334,18 @@ void discoveryRejectsMissingOrPartialIdentityCapabilities(final String capabilit | |
| } | ||
|
|
||
| @Test | ||
| void featureFlagProxyRequestNeverCarriesDirectCredentials() throws Exception { | ||
| void featureFlagProxyRequestCarriesSdkIdentityWithoutDirectCredentials() throws Exception { | ||
| final MockWebServer agent = new MockWebServer(); | ||
| agent.enqueue(new MockResponse().setResponseCode(200).setBody("{}")); | ||
| agent.start(); | ||
| try { | ||
| final FakeFeaturesDiscovery discovery = new FakeFeaturesDiscovery(V4_EVP_PROXY_ENDPOINT); | ||
| final BackendApiFactory factory = | ||
| new BackendApiFactory( | ||
| Config.get(), sharedCommunicationObjects(discovery, agent.url("/")), true); | ||
| Config.get(), | ||
| sharedCommunicationObjects(discovery, agent.url("/")), | ||
| sdkHeaders(), | ||
| true); | ||
| final BackendApi api = | ||
| factory.createEvpProxyApi( | ||
| Intake.EVENT_PLATFORM, | ||
|
|
@@ -355,6 +365,8 @@ void featureFlagProxyRequestNeverCarriesDirectCredentials() throws Exception { | |
| final RecordedRequest request = agent.takeRequest(); | ||
| assertEquals("/evp_proxy/v4/api/v2/exposures", request.getPath()); | ||
| assertEquals("event-platform-intake", request.getHeader("X-Datadog-EVP-Subdomain")); | ||
| assertEquals(JAVA_TRACING_LIBRARY, request.getHeader(ORIGIN_HEADER)); | ||
| assertEquals("test-version", request.getHeader(ORIGIN_VERSION_HEADER)); | ||
| assertNull(request.getHeader("DD-API-KEY")); | ||
| assertEquals(1, agent.getRequestCount()); | ||
| } finally { | ||
|
|
@@ -363,15 +375,55 @@ void featureFlagProxyRequestNeverCarriesDirectCredentials() throws Exception { | |
| } | ||
|
|
||
| @Test | ||
| void featureFlagDirectRequestCarriesCredentialsExactlyOnce() throws Exception { | ||
| void directIntakeSendsConfiguredRequestHeaders() throws Exception { | ||
| final MockWebServer intake = new MockWebServer(); | ||
| intake.enqueue(new MockResponse().setResponseCode(200).setBody("{}")); | ||
| intake.start(); | ||
| try { | ||
| final Properties properties = new Properties(); | ||
| properties.setProperty(API_KEY, "api-key"); | ||
| properties.setProperty( | ||
| CIVISIBILITY_AGENTLESS_URL, intake.url("/").toString().replaceAll("/$", "")); | ||
| final Config config = Config.get(properties); | ||
| final BackendApiFactory factory = | ||
| new BackendApiFactory( | ||
| config, | ||
| sharedCommunicationObjects(new FakeFeaturesDiscovery(null), null), | ||
| singletonMap(ORIGIN_HEADER, JAVA_TRACING_LIBRARY)); | ||
|
|
||
| // followRedirects=false mirrors the feature-flagging caller, so this also covers the | ||
| // interaction between the redirect-scoped client and the header interceptor. | ||
| final BackendApi api = factory.createDirectIntakeApi(Intake.API, false, false); | ||
|
|
||
| assertNotNull(api); | ||
| api.post( | ||
| "flagevaluation", | ||
| RequestBody.create(JSON, "{}".getBytes(StandardCharsets.UTF_8)), | ||
| stream -> null, | ||
| null, | ||
| false); | ||
|
|
||
| final RecordedRequest request = intake.takeRequest(); | ||
| assertEquals(JAVA_TRACING_LIBRARY, request.getHeader(ORIGIN_HEADER)); | ||
| assertEquals("api-key", request.getHeader("DD-API-KEY")); | ||
| } finally { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ⚪ Nit — test hygiene This test leaves one idle socket open to a dead port. The sibling test 40 lines down evicts its pool; this one does not. One line fixes it. Because Evidence — measured effect (executed)So: one idle socket to a dead port, held for the OkHttp keep-alive window. No non-daemon thread, so it cannot block JVM exit, and no leaked call. This is hygiene, not a leak. Two things the obvious fix gets wrongThe dispatcher line is a no-op here. Copying the sibling's teardown verbatim would add Worth knowing: the sibling's own dispatcher line buys little either. Its request goes through the synchronous
The derivation is not what allocates the pool. The derived client shares the parent's pool by reference, so the same idle socket would exist if the test used the parent directly. The reason to fix it is consistency with the sibling, not a cost introduced by this PR. Suggested change — compiled and run; Hoist the final SharedCommunicationObjects sco =
sharedCommunicationObjects(new FakeFeaturesDiscovery(null), null);
try {
// …
final BackendApiFactory factory =
new BackendApiFactory(config, sco, singletonMap(ORIGIN_HEADER, JAVA_TRACING_LIBRARY));
// …
} finally {
// The derived client shares the parent connection pool by reference, so evicting the
// parent pool closes the idle socket this test opened.
sco.getIntakeHttpClient().connectionPool().evictAll();
intake.shutdown();
} |
||
| intake.shutdown(); | ||
| } | ||
| } | ||
|
|
||
| @Test | ||
| void featureFlagDirectRequestCarriesCredentialsAndIdentityExactlyOnce() throws Exception { | ||
| final MockWebServer intake = new MockWebServer(); | ||
| intake.enqueue(new MockResponse().setResponseCode(500).setBody("failed")); | ||
| intake.enqueue(new MockResponse().setResponseCode(200).setBody("{}")); | ||
| intake.start(); | ||
| final OkHttpClient sharedClient = new OkHttpClient.Builder().build(); | ||
| final BackendApiFactory factory = | ||
| new BackendApiFactory( | ||
| Config.get(), sharedCommunicationObjects(new FakeFeaturesDiscovery(null), null), true); | ||
| Config.get(), | ||
| sharedCommunicationObjects(new FakeFeaturesDiscovery(null), null), | ||
| sdkHeaders(), | ||
| true); | ||
| final OkHttpClient directClient = factory.configureHttpClient(sharedClient); | ||
| try { | ||
| final IntakeApi api = | ||
|
|
@@ -396,6 +448,8 @@ void featureFlagDirectRequestCarriesCredentialsExactlyOnce() throws Exception { | |
| final RecordedRequest request = intake.takeRequest(); | ||
| assertEquals("/api/v2/flagevaluation", request.getPath()); | ||
| assertEquals("api-key", request.getHeader("DD-API-KEY")); | ||
| assertEquals("dd-trace-java", request.getHeader("DD-EVP-ORIGIN")); | ||
| assertEquals("test-version", request.getHeader("DD-EVP-ORIGIN-VERSION")); | ||
| assertNull(request.getHeader("X-Datadog-EVP-Subdomain")); | ||
| assertEquals(1, intake.getRequestCount()); | ||
| assertEquals(false, directClient.retryOnConnectionFailure()); | ||
|
|
@@ -523,6 +577,13 @@ private static SharedCommunicationObjects sharedCommunicationObjects( | |
| return sco; | ||
| } | ||
|
|
||
| private static Map<String, String> sdkHeaders() { | ||
| final Map<String, String> headers = new HashMap<>(2); | ||
| headers.put("DD-EVP-ORIGIN", "dd-trace-java"); | ||
| headers.put("DD-EVP-ORIGIN-VERSION", "test-version"); | ||
| return headers; | ||
| } | ||
|
|
||
| private static final class TestSharedCommunicationObjects extends SharedCommunicationObjects { | ||
| private final DDAgentFeaturesDiscovery discovery; | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | |||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,16 +1,23 @@ | |||||||||||||||||||||||
| package com.datadog.featureflag; | |||||||||||||||||||||||
|
|
|||||||||||||||||||||||
| import static datadog.communication.EvpProxy.JAVA_TRACING_LIBRARY; | |||||||||||||||||||||||
| import static datadog.communication.EvpProxy.ORIGIN_HEADER; | |||||||||||||||||||||||
| import static datadog.communication.EvpProxy.ORIGIN_VERSION_HEADER; | |||||||||||||||||||||||
| import static datadog.communication.ddagent.DDAgentFeaturesDiscovery.V2_EVP_PROXY_ENDPOINT; | |||||||||||||||||||||||
| import static datadog.trace.api.featureflag.config.FeatureFlaggingConfig.CONFIGURATION_SOURCE_AGENTLESS; | |||||||||||||||||||||||
| import static java.util.Collections.emptyList; | |||||||||||||||||||||||
| import static java.util.Arrays.asList; | |||||||||||||||||||||||
| 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.net.ConnectException; | |||||||||||||||||||||||
| import java.util.HashMap; | |||||||||||||||||||||||
| import java.util.Map; | |||||||||||||||||||||||
| import javax.annotation.Nullable; | |||||||||||||||||||||||
| import okhttp3.Response; | |||||||||||||||||||||||
| import org.slf4j.Logger; | |||||||||||||||||||||||
|
|
@@ -20,6 +27,7 @@ | ||||||||||||||||||||||
| final class FeatureFlagBackendApiFactory { | |||||||||||||||||||||||
|
|
|||||||||||||||||||||||
| private static final Logger LOGGER = LoggerFactory.getLogger(FeatureFlagBackendApiFactory.class); | |||||||||||||||||||||||
| private static final Map<String, String> REQUEST_HEADERS = requestHeaders(); | |||||||||||||||||||||||
|
|
|||||||||||||||||||||||
| /** Retries connection refusal only; automatic retries and redirects remain disabled. */ | |||||||||||||||||||||||
| static final HttpRetryPolicy.Factory PRE_SEND_RETRY = | |||||||||||||||||||||||
|
|
@@ -71,7 +79,7 @@ public boolean shouldRetry(@Nullable final Response response) { | ||||||||||||||||||||||
| final FeatureFlagRouteSelector routeSelector) { | |||||||||||||||||||||||
| this( | |||||||||||||||||||||||
| config, | |||||||||||||||||||||||
| new BackendApiFactory(config, sharedCommunicationObjects, true), | |||||||||||||||||||||||
| new BackendApiFactory(config, sharedCommunicationObjects, REQUEST_HEADERS, true), | |||||||||||||||||||||||
|
leoromanovsky marked this conversation as resolved.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 Comment from Claude working with Aaron Silverman: [P2] Please add a test that sends an exposure through the real writer and checks both identity headers on the wire. Today no test protects the identity headers that this line passes. If this line passes an empty map instead of The open thread at line 112 suggests a smoke test for this wiring. A unit test can do it. 1. Measured: three mutants pass the current tests, and the test below fails on each
The third mutant matters more after you restack onto #12477's head 2. Suggested test: for
|
|||||||||||||||||||||||
| eventType, | |||||||||||||||||||||||
| routeSelector); | |||||||||||||||||||||||
| } | |||||||||||||||||||||||
|
|
@@ -127,14 +135,22 @@ BackendApi create() { | ||||||||||||||||||||||
| @Nullable | |||||||||||||||||||||||
| private String discoverProxyEndpoint(final boolean forceDiscovery) { | |||||||||||||||||||||||
| try { | |||||||||||||||||||||||
| // Identity-forwarding requirements belong with SDK identity emission in the identity PR. | |||||||||||||||||||||||
| return backendApiFactory.discoverEvpProxyEndpoint(forceDiscovery, emptyList()); | |||||||||||||||||||||||
| // A local route must preserve the identity headers that this sender emits on every batch. | |||||||||||||||||||||||
| return backendApiFactory.discoverEvpProxyEndpoint( | |||||||||||||||||||||||
| forceDiscovery, asList(ORIGIN_HEADER, ORIGIN_VERSION_HEADER)); | |||||||||||||||||||||||
| } catch (final RuntimeException exception) { | |||||||||||||||||||||||
| LOGGER.debug("Could not discover the local Feature Flagging route", exception); | |||||||||||||||||||||||
| return null; | |||||||||||||||||||||||
| } | |||||||||||||||||||||||
| } | |||||||||||||||||||||||
|
|
|||||||||||||||||||||||
| private static Map<String, String> requestHeaders() { | |||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 Non-blocking suggestion — test coverage Delete the two The transport is covered; the contents are not. Evidence — mutation results (executed)
M2 proves the harness can go red. It just does not watch this method. Evidence — without a fixture, the version assertion misses a hardcoded
|
|||||||||||||||||||||||
| Tree state | ff-lib suite |
|---|---|
| Fix + fixture | 280/280 ✅ |
Fix + fixture, version source replaced by literal "0.0.0" |
1 ❌ |
| Fix + fixture, M1 re-applied | 1 ❌ |
One local caveat: build.gradle.kts:77-82 sets normalization { runtimeClasspath { ignore("**/*.version") } }. A change to the fixture alone does not invalidate the test task cache key. Use --no-build-cache --rerun-tasks while you iterate — that is how I ran all three rows above.
Scope — what this test does and does not cover
It covers the contents of the map. It does not cover the wiring: the test still passes if line 49 stops passing REQUEST_HEADERS to the constructor. Closing that needs a real BackendApiFactory, which this test class does not use.
A smoke test can close it. OpenFeatureProviderSmokeTest runs DD_FEATURE_FLAGS_CONFIGURATION_SOURCE=remote_config (line 46), which takes the fixed EVP v2 route and so bypasses the capability gate, and decodedEvpProxyMessageCallback() (line 56) already receives the RequestApi. RequestApi.getHeader(String) exists (TestHttpServer.groovy:450). Capturing request.getHeader('DD-EVP-ORIGIN') and asserting it in 'test open feature exposures' would cover the wiring end to end. I did not write that test.
With the current AbstractSmokeTest /info response, agentless mode rejects the discovered local proxy: AbstractSmokeTest.groovy:94-100 advertises only Content-Type, Accept-Encoding, Content-Encoding, User-Agent, DD-CI-PROVIDER-NAME, so the capability gate fails. Exercising that proxy route would need the mock advertisement updated. Direct intake is a separate path and is not blocked by this.
Why coverage metrics will not flag this
The static initializer at line 28 runs requestHeaders() whenever the class loads, so line coverage reports the method as covered although no assertion reads the values. Separately, datadog.communication.BackendApiFactory sits in excludedClassesCoverage (communication/build.gradle.kts:53).
Suggested change — compiled and run; kills M1.
FeatureFlagBackendApiFactory.java:112 — widen the method:
static Map<String, String> requestHeaders() {New file products/feature-flagging/feature-flagging-lib/src/test/resources/dd-java-agent.version:
TEST-SNAPSHOT~test
FeatureFlagBackendApiFactoryTest.java — add four imports (static org.junit.jupiter.api.Assertions.assertEquals, static org.junit.jupiter.api.Assertions.assertNotEquals, datadog.communication.ddagent.TracerVersion, java.util.Map) and the test:
@Test
void requestHeadersIdentifyTheJavaTracerAndItsVersion() {
final Map<String, String> headers = FeatureFlagBackendApiFactory.requestHeaders();
assertEquals("dd-trace-java", headers.get("DD-EVP-ORIGIN"));
assertEquals(TracerVersion.TRACER_VERSION, headers.get("DD-EVP-ORIGIN-VERSION"));
// Guards the premise: without src/test/resources/dd-java-agent.version the line above
// compares the "0.0.0" fallback with itself.
assertNotEquals("0.0.0", headers.get("DD-EVP-ORIGIN-VERSION"));
assertEquals(2, headers.size());
}The header name and the origin value stay raw literals on purpose. They pin the wire format from outside the constants.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Partially addressed in 1ac2d95866: the real-factory socket test now asserts both headers and exercises constructor wiring. The non-fallback version fixture is still missing, so I am leaving this coverage request open.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Non-blocking cleanup — dead API surface
Two of the four public constructors have no independent caller outside tests. Delete both 3-argument overloads now, while the API is new and nothing in this repository depends on its shape.
The one worth removing on its own merits is
(Config, SCO, Map). It implicitly selectssendOnce = false. The default explicit HTTP policy then permits up to five retries —new HttpRetryPolicy.Factory(5, 100, 2.0, true)at line 298, whose first parameter ismaxRetries, allowing up to six explicit attempts before accounting for retries internal to OkHttp — while the supplied OkHttp client's ownretryOnConnectionFailuresetting is left as it is. EVP callers can override the explicit policy, including withNEVER_RETRY. When retries are enabled, a payload with no idempotency key can produce duplicate events after an ambiguous failure, for example when the intake processes the request but the response times out. Nothing in the signature warns about it, and the Javadoc that explainssendOncesits on the 4-argument form only.Evidence — caller census (repo-wide grep)
(Config, SCO)LogsIntakeSystem:22,CiVisibilityServices:84,85, 2 JMH benchmarks(Config, SCO, Map)BackendApiFactoryTest:389(Config, SCO, boolean)BackendApiFactoryTest:323,:512(Config, SCO, Map, boolean)FeatureFlagBackendApiFactory:49Two honest caveats. The JMH benchmarks are not production code, they just live outside
src/test. And the map overload is not dead execution — the 2-argument constructor delegates to it today. The proposed change re-points that delegation at the 4-argument form.Evidence — the deletion compiles and passes (executed)
I applied the change in a scratch worktree, updated the three test call sites, and added
import static java.util.Collections.emptyMap;to the test file::communication:test— 235/235 ✅:products:feature-flagging:feature-flagging-lib:test— 279/279 ✅:dd-java-agent:agent-ci-visibility:compileJavacould not build in my offline worktree. It fails identically on the clean tree (unresolvablescala-library/kotlin-stdlib), and it uses only the retained 2-argument form.Scope note. This removes a footgun from a new API; it does not fix a live bug. The only production caller that needs
sendOncealready passestrueexplicitly (FeatureFlagBackendApiFactory:49), and the retained 2-argument form still defaults tofalsefor the existing CI Visibility and Logs Intake callers, which is correct for them.Suggested change — delete lines 37-47 and collapse the 2-argument form:
Then make the three test call sites explicit, keeping each one's existing arguments: