Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
02f2a4d
Populate source.name and source.version on flag_evaluations and expos…
vjfridge Aug 12, 2026
8ed8522
Tighten comments: drop ticket refs and redundant javadoc
vjfridge Aug 12, 2026
5dc3b91
Merge branch 'master' into vickie/ffl-2995-sdk-name-and-version-java
vjfridge Aug 13, 2026
4aa5a6f
fix(openfeature): emit source.name/version per-event, not in batch co…
vjfridge Aug 14, 2026
b1ba5af
Merge origin/master into vickie/ffl-2995-sdk-name-and-version-java
vjfridge Aug 28, 2026
a745b61
test(openfeature): verify flag evaluation source metadata
vjfridge Aug 28, 2026
b4724e9
fix(openfeature): send flag evaluation source headers
vjfridge Aug 28, 2026
38e9e1b
test(communication): cover backend header fallback
vjfridge Aug 28, 2026
3b940be
refactor(openfeature): configure SDK headers per backend
vjfridge Aug 28, 2026
1cb2c70
refactor(openfeature): inject SDK headers with HTTP client
vjfridge Aug 28, 2026
8f0cd1a
Merge remote-tracking branch 'origin/master' into vickie/ffl-2995-sdk…
vjfridge Sep 9, 2026
b4a467d
refactor(openfeature): share EVP origin header constants
vjfridge Sep 9, 2026
49eada3
test(communication): drop unrelated EVP proxy assertions
vjfridge Sep 9, 2026
a4b447e
Publish dd-openfeature PR snapshots (#12400)
leoromanovsky Sep 10, 2026
59b0330
Fix NullPointerException in Exception Replay (#12429)
jpbempel Sep 10, 2026
2df9ed5
Add team freeze guard workflow (#12437)
cbeauchesne Sep 10, 2026
7c6ce51
Fix NPE in DatabaseClientDecorator when JDBC connection has no db typ…
dougqh Sep 10, 2026
20463f5
Migrate telemetry groovy files to java part 1 (#12430)
jpbempel Sep 10, 2026
4b3e216
Return empty method lines instead of throwing when class bytecode res…
dougqh Sep 10, 2026
89f324c
Use ConcurrentHashtable for telemetry log (#12367)
bric3 Sep 11, 2026
169d834
Report v1 SDK custom events as sdk_version:v1 in telemetry (#12438)
claponcet Sep 11, 2026
4eb5971
fix tibco bw instrumentation for 5.16 (#12449)
vandonr Sep 11, 2026
40a0f9d
Stop AppSecInterceptor from silently retrying failed okhttp requests …
dougqh Sep 11, 2026
6ac2b95
Carry the intercepted HTTP status as an int, rendering it on demand (…
dougqh Sep 11, 2026
75df78b
Return non-null traceConfig from ExtractedSpan (#12413)
mcculls Sep 11, 2026
b0ce4bb
Prevent duplicate Kotlin coroutine continuation release (#12466)
amarziali Sep 11, 2026
64c7b9f
Use ObjectStore to track context bound to arbitrary objects. (#12463)
mcculls Sep 11, 2026
3110972
Introduce SpanPrototype: baked-once constant span-tag descriptor (pha…
dougqh Sep 11, 2026
01d58b7
Merge feature flag EVP identity groundwork
leoromanovsky Sep 12, 2026
1a58e69
Harden Feature Flags EVP transport
leoromanovsky Sep 9, 2026
6b57b41
Drain Feature Flags exposures on shutdown
leoromanovsky Sep 9, 2026
d99255f
Keep agent shutdown active when tracing is disabled
leoromanovsky Sep 9, 2026
84e50e3
Validate direct intake DNS site suffixes
leoromanovsky Sep 9, 2026
f1a7696
Complete Feature Flags EVP route hardening
leoromanovsky Sep 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .agents/skills/migrate-groovy-to-java/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ When converting Groovy code to Java code, make sure that:
- Migrate the named Spock clauses if they exist as inline comments in the Java unit test
- When Groovy tests navigate a JSON request body through helpers like `asMap()` / `asLong()` / `asList()`, check whether `json-unit-assertj` (`libs.json.unit.assertj`) is already in the module's build file. If it is, add a method that returns the raw JSON string and use `assertThatJson(json).node("some.nested.field").isEqualTo(value)` directly instead of the map traversal.
- Groovy's `[key: val]` map literals use a `LinkedHashMap`. When the test doesn't care about insertion order, use `singletonMap` for a single entry or `HashMap` for two or more. If a helper method builds these maps, add a two-arg overload rather than scattering `new LinkedHashMap<>()` constructions through test bodies.
- The Spock construct `clean:` section needs to wrap the unit test with a try...finally block to respect the behavior.

TableTest usage
Import: `import org.tabletest.junit.TableTest;`
Expand Down
10 changes: 10 additions & 0 deletions .github/workflows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,16 @@ _Recovery:_ Re-write the new Groovy files in Java / JUnit. To override this chec

_Notes:_ The override label skips the workflow entirely.

### team-freeze-guard [🔗](team-freeze-guard.yaml)

_Trigger:_ When a pull request is opened, reopened, synchronized, labeled, unlabeled, or marked ready for review.

_Action:_ Fail the check if the pull request author or the last committer belongs to a team currently listed in the `frozen-teams` input, blocking the PR from merging.

_Recovery:_ If your PR is not dedicated to fix CI issue, it'll be block until the incident is resolved. If your Pr aims to fix CI issues, add one of the configured labels: `comp: testing`, `comp: tooling`, `tag: flaky test`, `tag: flaky test/disabled` to the pull request.

_Notes:_ Configure frozen teams by editing the `frozen-teams` input in the workflow file (empty string means no team is frozen; otherwise list `@DataDog/<team>` entries one per line).

## Code Quality and Security

### analyze-changes [🔗](analyze-changes.yaml)
Expand Down
41 changes: 41 additions & 0 deletions .github/workflows/team-freeze-guard.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# How to configure:
#
# When no team is frozen, set `frozen-teams` to an empty string:
# frozen-teams: ""
#
# When one or more teams are frozen, add them one per line with the @DataDog prefix:
#
# frozen-teams: |
# @DataDog/team-a
# @DataDog/team-b

name: Team freeze guard

on:
pull_request_target:
types:
- opened
- reopened
- synchronize
- labeled
- unlabeled
- ready_for_review

permissions:
id-token: write
contents: read

jobs:
team-freeze-guard:
name: Team freeze guard
runs-on: ubuntu-latest

steps:
- uses: DataDog/team-freeze-guard@e4ff51a72cac229b238e5a302af0d3d955ae5b54 # v0.0.4
with:
bypass-labels: |
comp: testing
comp: tooling
tag: flaky test
tag: flaky test/disabled
frozen-teams: ""
11 changes: 10 additions & 1 deletion .gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ build:
script:
- if [ $CI_PIPELINE_SOURCE == "schedule" ] ; then ./gradlew resolveAndLockAll --write-locks $GRADLE_ARGS; fi
- ./gradlew --version
- ./gradlew clean :dd-java-agent:shadowJar :dd-java-agent:check :dd-trace-api:jar :dd-trace-ot:shadowJar -PskipTests -x spotlessCheck $GRADLE_ARGS
- ./gradlew clean :dd-java-agent:shadowJar :dd-java-agent:check :dd-trace-api:jar :dd-trace-ot:shadowJar :products:feature-flagging:feature-flagging-api:jar -PskipTests -x spotlessCheck $GRADLE_ARGS
- echo UPSTREAM_TRACER_VERSION=$(java -jar workspace/dd-java-agent/build/libs/*.jar) >> upstream.env
- echo "BUILD_JOB_NAME=$CI_JOB_NAME" >> build.env
- echo "BUILD_JOB_ID=$CI_JOB_ID" >> build.env
Expand All @@ -365,6 +365,7 @@ build:
- 'workspace/dd-java-agent/build/libs/*.jar'
- 'workspace/dd-trace-api/build/libs/*.jar'
- 'workspace/dd-trace-ot/build/libs/*.jar'
- 'workspace/products/feature-flagging/feature-flagging-api/build/libs/*.jar'
- 'upstream.env'
- '.gradle/daemon/*/*.out.log'
reports:
Expand Down Expand Up @@ -481,9 +482,11 @@ publish-artifacts-to-s3:
- aws s3 cp workspace/dd-java-agent/build/libs/dd-java-agent-${VERSION}.jar s3://dd-trace-java-builds/${CI_COMMIT_REF_NAME}/dd-java-agent.jar
- aws s3 cp workspace/dd-trace-api/build/libs/dd-trace-api-${VERSION}.jar s3://dd-trace-java-builds/${CI_COMMIT_REF_NAME}/dd-trace-api.jar
- aws s3 cp workspace/dd-trace-ot/build/libs/dd-trace-ot-${VERSION}.jar s3://dd-trace-java-builds/${CI_COMMIT_REF_NAME}/dd-trace-ot.jar
- aws s3 cp workspace/products/feature-flagging/feature-flagging-api/build/libs/dd-openfeature-${VERSION}.jar s3://dd-trace-java-builds/${CI_COMMIT_REF_NAME}/dd-openfeature.jar
- aws s3 cp workspace/dd-java-agent/build/libs/dd-java-agent-${VERSION}.jar s3://dd-trace-java-builds/${CI_PIPELINE_ID}/dd-java-agent.jar
- aws s3 cp workspace/dd-trace-api/build/libs/dd-trace-api-${VERSION}.jar s3://dd-trace-java-builds/${CI_PIPELINE_ID}/dd-trace-api.jar
- aws s3 cp workspace/dd-trace-ot/build/libs/dd-trace-ot-${VERSION}.jar s3://dd-trace-java-builds/${CI_PIPELINE_ID}/dd-trace-ot.jar
- aws s3 cp workspace/products/feature-flagging/feature-flagging-api/build/libs/dd-openfeature-${VERSION}.jar s3://dd-trace-java-builds/${CI_PIPELINE_ID}/dd-openfeature.jar
- |
cat << EOF > links.json
{
Expand All @@ -493,6 +496,12 @@ publish-artifacts-to-s3:
"label": "Public Link to dd-java-agent.jar",
"url": "https://s3.us-east-1.amazonaws.com/dd-trace-java-builds/${CI_PIPELINE_ID}/dd-java-agent.jar"
}
},
{
"external_link": {
"label": "Public Link to dd-openfeature.jar",
"url": "https://s3.us-east-1.amazonaws.com/dd-trace-java-builds/${CI_PIPELINE_ID}/dd-openfeature.jar"
}
}
]
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,61 @@
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 okhttp3.OkHttpClient;
import okhttp3.Request;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class BackendApiFactory {

private static final Logger log = LoggerFactory.getLogger(BackendApiFactory.class);
private static final int MAX_DNS_LABEL_LENGTH = 63;
private static final int MAX_DNS_HOST_LENGTH = 253;

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, emptyMap());
}

public BackendApiFactory(
Config config,
SharedCommunicationObjects sharedCommunicationObjects,
Map<String, String> requestHeaders) {
this(config, sharedCommunicationObjects, requestHeaders, false);
}

/**
* Creates a backend factory with per-request headers and optional send-once transport semantics.
*
* <p>When {@code sendOnce} is true, both the explicit HTTP retry policy and OkHttp's automatic
* connection retry are disabled. This is required for event payloads that do not carry an
* idempotency key.
*/
public BackendApiFactory(
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;
}

public @Nullable BackendApi createBackendApi(Intake intake) {
Expand Down Expand Up @@ -67,7 +101,9 @@ public BackendApi createDirectIntakeApi(
apiKey,
traceId,
retryPolicyFactory(),
directIntakeHttpClient(sharedCommunicationObjects.getIntakeHttpClient(), followRedirects),
configureHttpClient(
directIntakeHttpClient(
sharedCommunicationObjects.getIntakeHttpClient(), followRedirects)),
responseCompression);
}

Expand All @@ -87,11 +123,14 @@ private static HttpUrl buildDirectIntakeUrl(Intake intake, Config config) {
}

static HttpUrl buildEventPlatformIntakeUrl(String site) {
if (site == null || site.isEmpty()) {
if (!isValidDnsSuffix(site)) {
throw new IllegalArgumentException("Invalid Datadog site");
}

String expectedHost = Intake.EVENT_PLATFORM.getUrlPrefix() + "." + site;
if (expectedHost.length() > MAX_DNS_HOST_LENGTH) {
throw new IllegalArgumentException("Invalid Datadog site");
}
HttpUrl url =
new HttpUrl.Builder()
.scheme("https")
Expand All @@ -106,6 +145,38 @@ static HttpUrl buildEventPlatformIntakeUrl(String site) {
return url;
}

private static boolean isValidDnsSuffix(@Nullable String site) {
if (site == null || site.isEmpty()) {
return false;
}

int labelLength = 0;
for (int i = 0; i < site.length(); i++) {
final char character = site.charAt(i);
if (character == '.') {
if (labelLength == 0 || labelLength > MAX_DNS_LABEL_LENGTH || site.charAt(i - 1) == '-') {
return false;
}
labelLength = 0;
} else {
if ((!isAsciiLetterOrDigit(character) && character != '-')
|| (labelLength == 0 && character == '-')) {
return false;
}
labelLength++;
}
}
return labelLength > 0
&& labelLength <= MAX_DNS_LABEL_LENGTH
&& site.charAt(site.length() - 1) != '-';
}

private static boolean isAsciiLetterOrDigit(final char character) {
return (character >= 'a' && character <= 'z')
|| (character >= 'A' && character <= 'Z')
|| (character >= '0' && character <= '9');
}

/** Creates an API client that uses the specified retry policy with a compatible local proxy. */
public @Nullable BackendApi createEvpProxyApi(Intake intake) {
return createEvpProxyApi(intake, true);
Expand All @@ -119,32 +190,104 @@ static HttpUrl buildEventPlatformIntakeUrl(String site) {
/** Creates an API client that sends data through a compatible local EVP proxy. */
public @Nullable BackendApi createEvpProxyApi(
Intake intake, boolean responseCompression, HttpRetryPolicy.Factory retryPolicyFactory) {
return createEvpProxyApi(intake, responseCompression, retryPolicyFactory, false, false);
}

/**
* Creates an EVP proxy client after Agent discovery, optionally forcing a fresh discovery and
* requiring the Agent to advertise every configured request header.
*
* <p>The {@code forceDiscovery} form is intended for bounded unavailable-route recovery probes.
*/
public @Nullable BackendApi createEvpProxyApi(
Intake intake,
boolean responseCompression,
HttpRetryPolicy.Factory retryPolicyFactory,
boolean forceDiscovery,
boolean requireConfiguredRequestHeaders) {
DDAgentFeaturesDiscovery featuresDiscovery =
sharedCommunicationObjects.featuresDiscovery(config);
featuresDiscovery.discoverIfOutdated();
if (!featuresDiscovery.supportsEvpProxy()) {
return null;
if (forceDiscovery) {
featuresDiscovery.discover();
} else {
featuresDiscovery.discoverIfOutdated();
}
String evpProxyEndpoint = featuresDiscovery.getEvpProxyEndpoint();
if (evpProxyEndpoint != null
&& requireConfiguredRequestHeaders
&& !featuresDiscovery.supportsEvpProxyHeaders(requestHeaders.keySet())) {
evpProxyEndpoint = null;
}
if (evpProxyEndpoint == null) {
return null;
}

return createEvpProxyApi(intake, responseCompression, retryPolicyFactory, evpProxyEndpoint);
}

/** Creates an EVP proxy client for a fixed compatibility endpoint without Agent discovery. */
public BackendApi createEvpProxyApiForEndpoint(
Intake intake,
boolean responseCompression,
HttpRetryPolicy.Factory retryPolicyFactory,
String evpProxyEndpoint) {
return createEvpProxyApi(intake, responseCompression, retryPolicyFactory, evpProxyEndpoint);
}

private BackendApi createEvpProxyApi(
Intake intake,
boolean responseCompression,
HttpRetryPolicy.Factory retryPolicyFactory,
String evpProxyEndpoint) {
String traceId = config.getIdGenerationStrategy().generateTraceId().toString();
log.debug(
"Creating EVP proxy client for {} using endpoint {} with responseCompression={}",
intake,
evpProxyEndpoint,
responseCompression);
HttpUrl evpProxyUrl = sharedCommunicationObjects.agentUrl.resolve(evpProxyEndpoint);
HttpUrl evpProxyUrl = appendPath(sharedCommunicationObjects.agentUrl, evpProxyEndpoint);
String subdomain = intake.getUrlPrefix();
return new EvpProxyApi(
traceId,
evpProxyUrl,
subdomain,
retryPolicyFactory,
sharedCommunicationObjects.agentHttpClient,
sendOnce ? HttpRetryPolicy.Factory.NEVER_RETRY : retryPolicyFactory,
configureHttpClient(sharedCommunicationObjects.agentHttpClient),
responseCompression);
}

private static HttpRetryPolicy.Factory retryPolicyFactory() {
return new HttpRetryPolicy.Factory(5, 100, 2.0, true);
static HttpUrl appendPath(final HttpUrl baseUrl, final String path) {
int firstCharacter = 0;
while (firstCharacter < path.length() && path.charAt(firstCharacter) == '/') {
firstCharacter++;
}
return baseUrl.newBuilder().addPathSegments(path.substring(firstCharacter)).build();
}

OkHttpClient configureHttpClient(final OkHttpClient httpClient) {
if (requestHeaders.isEmpty() && !sendOnce) {
return httpClient;
}
final OkHttpClient.Builder builder = httpClient.newBuilder();
if (sendOnce) {
builder.retryOnConnectionFailure(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() {
return sendOnce
? HttpRetryPolicy.Factory.NEVER_RETRY
: new HttpRetryPolicy.Factory(5, 100, 2.0, true);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ public final class EvpProxy {

public static final String SUBDOMAIN_HEADER = "X-Datadog-EVP-Subdomain";

/** Identifies the SDK that produced an EVP request. */
public static final String ORIGIN_HEADER = "DD-EVP-ORIGIN";

/** Identifies the version of the SDK that produced an EVP request. */
public static final String ORIGIN_VERSION_HEADER = "DD-EVP-ORIGIN-VERSION";

/** Origin header value identifying this tracing library. */
public static final String JAVA_TRACING_LIBRARY = "dd-trace-java";

/**
* Default SDK-side target for uncompressed EVP request bodies. Writers may split batches at or
* below this size to keep Agent proxy requests comfortably bounded.
Expand Down
Loading