Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions metadata/supported-configurations.json
Original file line number Diff line number Diff line change
Expand Up @@ -1604,6 +1604,14 @@
"aliases": []
}
],
"DD_FEATURE_FLAGS_EVALUATION_TELEMETRY_PER_SUBJECT_OPT_OUT_ENABLED": [
{
"version": "A",
"type": "boolean",
"default": "false",
"aliases": []
}
],
"DD_FEATURE_FLAGS_EXPOSURES_DATADOG_LOGGING_ENABLED": [
{
"version": "A",
Expand Down
51 changes: 51 additions & 0 deletions products/feature-flagging/feature-flagging-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,57 @@ boolean enabled = client.getBooleanValue("my-feature", false,
new MutableContext("user-123"));
```

## Per-subject evaluation telemetry opt-out

Applications can keep flag evaluation enabled while excluding selected subjects from evaluation
telemetry. Configure the provider once, then pass the decision with each evaluation:

```java
import datadog.trace.api.openfeature.Provider;
import dev.openfeature.sdk.Client;
import dev.openfeature.sdk.ImmutableContext;
import dev.openfeature.sdk.OpenFeatureAPI;
import dev.openfeature.sdk.Value;
import java.util.Collections;

OpenFeatureAPI api = OpenFeatureAPI.getInstance();
api.setProviderAndWait(new Provider(
new Provider.Options().enableEvaluationTelemetryOptOutParameter(true)));
Client client = api.getClient();

// In a request handler, the application owns its consent and eligibility rules.
boolean optedOut = user.hasDeclinedEvaluationTelemetry();
boolean enabled = client.getBooleanValue("my-feature", false,
new ImmutableContext(user.getId(), Collections.singletonMap(
"dd_evaluation_telemetry_opt_out", new Value(optedOut))));
```

Use the per-evaluation context for concurrent requests. Do not replace the global context for each
subject. The key is exactly `dd_evaluation_telemetry_opt_out`, as in other Datadog SDKs.

Alternatively, enable support through deployment configuration and use `new Provider()`:

```shell
DD_FEATURE_FLAGS_EVALUATION_TELEMETRY_PER_SUBJECT_OPT_OUT_ENABLED=true
```

The equivalent JVM property is `-Ddd.feature.flags.evaluation.telemetry.per.subject.opt.out.enabled=true`.
The provider reads the normal Datadog configuration sources at construction. The default is `false`.
An explicit provider option overrides these sources, including an explicit `false`.
This setting only enables support for the subject attribute. It does not suppress telemetry for all subjects.

Only boolean `true` suppresses telemetry. Missing, false, null, and other types preserve telemetry.
The provider suppresses telemetry only when the option is enabled before provider construction.

The attribute remains available for targeting and exposure reporting with either activation setting.
Existing privacy and reporting settings apply. Targeting rules can use the attribute to select
assignments. Normal exposure deduplication applies when its value changes.

Opt-out suppresses new evaluation EVP events and counts, OpenTelemetry evaluation metrics, and
automatic flag-related span enrichment. It does not change flag values, evaluation details,
application defaults, experiment assignment, or exposure logging. Application hooks and unrelated
tracing remain active. Previously recorded or queued data is not removed.

## Evaluation metrics

When `DD_METRICS_OTEL_ENABLED=true` and the OpenTelemetry API is on the classpath, the provider
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ dependencies {
// branch cannot be driven.
testImplementation(project(":products:feature-flagging:feature-flagging-config"))
testImplementation(project(":utils:config-utils"))
testImplementation(project(":components:environment"))
testImplementation("io.opentelemetry:opentelemetry-api:1.47.0")
testImplementation(libs.bundles.junit5)
testImplementation(libs.bundles.mockito)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package datadog.trace.api.openfeature;

import dev.openfeature.sdk.EvaluationContext;
import dev.openfeature.sdk.FlagEvaluationDetails;
import dev.openfeature.sdk.Hook;
import dev.openfeature.sdk.HookContext;
import dev.openfeature.sdk.Value;
import java.util.Map;
import java.util.Optional;

/** Gates a Datadog finally-only evaluation telemetry hook, not exposure or application hooks. */
final class EvaluationTelemetryOptOutHook implements Hook<Object> {
private static final String PARAMETER = "dd_evaluation_telemetry_opt_out";
private static final String DECISION = "datadog.evaluationTelemetryOptOut";
private final Hook<Object> delegate;

EvaluationTelemetryOptOutHook(final Hook<Object> delegate) {
this.delegate = delegate;
}

@Override
public Optional<EvaluationContext> before(
final HookContext<Object> context, final Map<String, Object> hints) {
// Provider before hooks see the merged context, including application before hooks.
// HookData belongs to this evaluation, including nested and concurrent calls.
context.getHookData().set(DECISION, isOptedOut(context.getCtx()));
return Optional.empty();
}

@Override
public void error(
final HookContext<Object> context, final Exception error, final Map<String, Object> hints) {
// A failing application before hook can prevent our before hook from running.
// Preserve its decision before application error hooks can change the context.
if (context.getHookData().get(DECISION) == null) {
context.getHookData().set(DECISION, isOptedOut(context.getCtx()));
}
}

@Override
public void finallyAfter(
final HookContext<Object> context,
final FlagEvaluationDetails<Object> details,
final Map<String, Object> hints) {
if (Boolean.TRUE.equals(context.getHookData().get(DECISION)) || isOptedOut(context.getCtx())) {
return;
}
delegate.finallyAfter(context, details, hints);
}

private static boolean isOptedOut(final EvaluationContext context) {
final Value value = context == null ? null : context.getValue(PARAMETER);
return value != null && value.isBoolean() && Boolean.TRUE.equals(value.asBoolean());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import static java.util.concurrent.TimeUnit.SECONDS;

import datadog.trace.api.featureflag.config.FeatureFlaggingConfig;
import datadog.trace.bootstrap.config.provider.ConfigProvider;
import de.thetaphi.forbiddenapis.SuppressForbidden;
import dev.openfeature.sdk.ErrorCode;
import dev.openfeature.sdk.EvaluationContext;
Expand Down Expand Up @@ -104,6 +106,12 @@ public Provider(final Options options) {
if (spanEnrichmentHook != null) {
hooks.add(spanEnrichmentHook);
}
if (options.isEvaluationTelemetryOptOutParameterEnabled()) {
for (int i = 0; i < hooks.size(); i++) {
hooks.set(i, new EvaluationTelemetryOptOutHook(hooks.get(i)));
}
}
// Exposure logging is independent of evaluation telemetry opt-out.
try {
hooks.add(buildExposureLoggingHook());
} catch (LinkageError e) {
Expand Down Expand Up @@ -312,8 +320,47 @@ private enum InitializationState {

public static class Options {

private long timeout;
private TimeUnit unit;
private long timeout = 30;
private TimeUnit unit = SECONDS;
private Boolean evaluationTelemetryOptOutParameterEnabled;

/**
* Enables per-evaluation telemetry opt-out through the {@code dd_evaluation_telemetry_opt_out}
* context attribute. Overrides {@code
* DD_FEATURE_FLAGS_EVALUATION_TELEMETRY_PER_SUBJECT_OPT_OUT_ENABLED}, including when false.
* Without an explicit option, the provider reads configuration at construction. Disabled by
* default.
*
* <p>Only boolean {@code true} suppresses new Datadog evaluation events, evaluation metrics,
* and automatic flag span enrichment. Missing or other values preserve telemetry. This does not
* change flag results, exposure logging, application hooks, or previously recorded data.
*
* <p>The attribute remains available for targeting and exposure reporting. Existing privacy and
* reporting settings apply.
*
* @param enabled whether to honor the attribute; configure before constructing the provider
* @return these options
*/
public Options enableEvaluationTelemetryOptOutParameter(final boolean enabled) {
this.evaluationTelemetryOptOutParameterEnabled = enabled;
return this;
}

private boolean isEvaluationTelemetryOptOutParameterEnabled() {
if (evaluationTelemetryOptOutParameterEnabled != null) {
return evaluationTelemetryOptOutParameterEnabled;
}
try {
return ConfigProvider.getInstance()
.getBoolean(
FeatureFlaggingConfig
.FEATURE_FLAGS_EVALUATION_TELEMETRY_PER_SUBJECT_OPT_OUT_ENABLED,
false);
} catch (final LinkageError | Exception e) {
// Keep provider construction compatible when agent configuration classes are unavailable.
return false;
}
}

public Options initTimeout(final long timeout, final TimeUnit unit) {
this.timeout = timeout;
Expand Down
Loading
Loading