Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,13 @@ public void finallyAfter(
ctx != null && ctx.getCtx() != null ? ctx.getCtx().getTargetingKey() : null;
final Map<String, Value> attrs = snapshotAttrs(ctx);

// Snapshot the PII consent flag now, on the evaluation thread, so it is pinned to the
// configuration active at evaluation time. The event is drained and flushed later, by which
// point a subsequent RC update may have changed CURRENT_CONFIG; reading consent here (not at
// drain/flush) is what makes the hashed-vs-raw decision faithful to the evaluation.
final boolean observeFullEvaluationData =
FeatureFlaggingGateway.isObserveFullEvaluationDataEnabled();

w.enqueue(
new FlagEvalEvent(
flagKey,
Expand All @@ -117,6 +124,7 @@ public void finallyAfter(
targetingKey,
errorMessage,
evalTimeMs,
observeFullEvaluationData,
() -> extractAttrs(attrs)));
} catch (LinkageError | Exception e) {
// Never let EVP recording break flag evaluation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ public void testNoAllocations() {
flags.put("null-allocation", new Flag("target", true, null, null, null));
flags.put("empty-allocation", new Flag("target", true, null, null, emptyList()));
final DDEvaluator evaluator = new DDEvaluator(mock(Runnable.class));
evaluator.accept(new ServerConfiguration("", "", null, flags));
evaluator.accept(new ServerConfiguration("", "", false, null, flags));

final EvaluationContext ctx = new MutableContext("target").setTargetingKey("allocation");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import datadog.trace.api.featureflag.FeatureFlaggingGateway;
import datadog.trace.api.featureflag.flagevaluation.FlagEvalEvent;
import datadog.trace.api.featureflag.flagevaluation.FlagEvaluationWriter;
import datadog.trace.api.featureflag.ufc.v1.ServerConfiguration;
import dev.openfeature.sdk.ErrorCode;
import dev.openfeature.sdk.FlagEvaluationDetails;
import dev.openfeature.sdk.FlagValueType;
Expand Down Expand Up @@ -49,6 +50,9 @@ void enableFlagEvaluationEnqueue() {
@AfterEach
void resetFlagEvaluationEnqueue() {
FeatureFlaggingGateway.setFlagEvaluationEnqueueEnabled(true);
// Clear any dispatched UFC so an observeFullEvaluationData value can't leak into other tests
// that share the static gateway.
FeatureFlaggingGateway.dispatch((ServerConfiguration) null);
}

// ---- helpers ----
Expand Down Expand Up @@ -457,4 +461,54 @@ void contextAttributesUseEnqueueTimeSnapshot() {
assertFalse(attrs.containsKey("profile.late"));
assertFalse(attrs.containsKey("cohorts[1]"));
}

// ---- observeFullEvaluationData is snapshotted from the gateway at evaluation time ----

@Test
void snapshotsObserveFullEvaluationDataTrueFromGatewayAtEvaluationTime() {
FeatureFlaggingGateway.dispatch(observeConfig(true));
try {
assertTrue(enqueuedEvent().observeFullEvaluationData);
} finally {
FeatureFlaggingGateway.dispatch((ServerConfiguration) null);
}
}

@Test
void snapshotsObserveFullEvaluationDataFalseFromGatewayAtEvaluationTime() {
FeatureFlaggingGateway.dispatch(observeConfig(false));
try {
assertFalse(enqueuedEvent().observeFullEvaluationData);
} finally {
FeatureFlaggingGateway.dispatch((ServerConfiguration) null);
}
}

@Test
void observeFullEvaluationDataDefaultsToFalseWhenNoConfigDispatched() {
// No UFC dispatched: the gateway reports the privacy-preserving default and the hook stamps it.
FeatureFlaggingGateway.dispatch((ServerConfiguration) null);
assertFalse(enqueuedEvent().observeFullEvaluationData);
}

/** Fires the hook once for a simple targeted evaluation and returns the enqueued event. */
private FlagEvalEvent enqueuedEvent() {
final AtomicReference<FlagEvalEvent> captured = new AtomicReference<>();
final FlagEvalLoggingHook<Object> hook = hookWithWriter(capturingWriter(captured));
hook.finallyAfter(
hookCtxWithTargetingKey("obs-flag", "user-1"),
details("obs-flag", "on", "on", Reason.TARGETING_MATCH.name(), null),
Collections.emptyMap());
assertNotNull(captured.get(), "writer.enqueue must be called once");
return captured.get();
}

private static ServerConfiguration observeConfig(final boolean observeFullEvaluationData) {
return new ServerConfiguration(
"2024-04-17T19:40:53.716Z",
"SERVER",
observeFullEvaluationData,
null,
Collections.emptyMap());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,16 @@ public static boolean isFlagEvaluationEnqueueEnabled() {
return flagEvalEnqueueEnabled;
}

/**
* Returns whether the currently active UFC environment has {@code observeFullEvaluationData}
* enabled. {@code false} (privacy-preserving default) when no UFC has been dispatched yet or when
* the field was absent/false on the last dispatched configuration.
*/
public static boolean isObserveFullEvaluationDataEnabled() {
final ServerConfiguration current = CURRENT_CONFIG.get();
return current != null && current.observeFullEvaluationData;
}

public static void addSpanEnrichmentListener(final SpanEnrichmentListener listener) {
SPAN_ENRICHMENT_LISTENERS.add(listener);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,18 +50,30 @@ public final class FlagEvalEvent {
*/
public final Map<String, Object> attrs;

/**
* Whether the UFC environment active <em>at evaluation time</em> had {@code
* observeFullEvaluationData} enabled. Snapshotted on the evaluation thread (from {@code
* FeatureFlaggingGateway.isObserveFullEvaluationDataEnabled()}) so the consent decision is pinned
* to the instant the flag was evaluated, not to whatever configuration happens to be active when
* the event is later drained and flushed. {@code false} is the privacy-preserving default: when
* off, the targeting key is hashed and the per-evaluation context is omitted on emission.
*/
public final boolean observeFullEvaluationData;

private final Supplier<Map<String, Object>> attrsSupplier;

/** Convenience constructor; consent defaults to the privacy-preserving {@code false}. */
public FlagEvalEvent(
final String flagKey,
final String variant,
final String allocationKey,
final String targetingKey,
final long evalTimeMs,
final Map<String, Object> attrs) {
this(flagKey, variant, allocationKey, targetingKey, null, evalTimeMs, attrs);
this(flagKey, variant, allocationKey, targetingKey, null, evalTimeMs, false, attrs);
}

/** Convenience constructor; consent defaults to the privacy-preserving {@code false}. */
public FlagEvalEvent(
final String flagKey,
final String variant,
Expand All @@ -70,30 +82,65 @@ public FlagEvalEvent(
final String errorMessage,
final long evalTimeMs,
final Map<String, Object> attrs) {
this(flagKey, variant, allocationKey, targetingKey, errorMessage, evalTimeMs, false, attrs);
}

public FlagEvalEvent(
final String flagKey,
final String variant,
final String allocationKey,
final String targetingKey,
final String errorMessage,
final long evalTimeMs,
final boolean observeFullEvaluationData,
final Map<String, Object> attrs) {
this.flagKey = flagKey;
this.variant = variant;
this.allocationKey = allocationKey;
this.targetingKey = targetingKey;
this.errorMessage = errorMessage;
this.evalTimeMs = evalTimeMs;
this.observeFullEvaluationData = observeFullEvaluationData;
this.attrs = attrs != null ? attrs : Collections.emptyMap();
this.attrsSupplier = null;
}

/** Convenience constructor; consent defaults to the privacy-preserving {@code false}. */
public FlagEvalEvent(
final String flagKey,
final String variant,
final String allocationKey,
final String targetingKey,
final String errorMessage,
final long evalTimeMs,
final Supplier<Map<String, Object>> attrsSupplier) {
this(
flagKey,
variant,
allocationKey,
targetingKey,
errorMessage,
evalTimeMs,
false,
attrsSupplier);
}

public FlagEvalEvent(
final String flagKey,
final String variant,
final String allocationKey,
final String targetingKey,
final String errorMessage,
final long evalTimeMs,
final boolean observeFullEvaluationData,
final Supplier<Map<String, Object>> attrsSupplier) {
this.flagKey = flagKey;
this.variant = variant;
this.allocationKey = allocationKey;
this.targetingKey = targetingKey;
this.errorMessage = errorMessage;
this.evalTimeMs = evalTimeMs;
this.observeFullEvaluationData = observeFullEvaluationData;
this.attrs = Collections.emptyMap();
this.attrsSupplier = attrsSupplier;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,19 @@
public class ServerConfiguration {
public final String createdAt;
public final String format;
public final boolean observeFullEvaluationData;
public final Environment environment;
public final Map<String, Flag> flags;

public ServerConfiguration(
final String createdAt,
final String format,
final boolean observeFullEvaluationData,
final Environment environment,
final Map<String, Flag> flags) {
this.createdAt = createdAt;
this.format = format;
this.observeFullEvaluationData = observeFullEvaluationData;
this.environment = environment;
this.flags = flags;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package datadog.trace.api.featureflag;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;

import datadog.trace.api.featureflag.exposure.ExposureEvent;
import datadog.trace.api.featureflag.ufc.v1.ServerConfiguration;
import java.util.Collections;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -96,6 +99,25 @@ void testAttachingASpanEnrichmentListener() {
verifyNoMoreInteractions(spanEnrichmentListener);
}

@Test
void isObserveFullEvaluationDataEnabledDefaultsToFalseWithNoConfig() {
FeatureFlaggingGateway.dispatch((ServerConfiguration) null);
assertFalse(FeatureFlaggingGateway.isObserveFullEvaluationDataEnabled());
}

@Test
void isObserveFullEvaluationDataEnabledReflectsLastDispatchedConfig() {
final ServerConfiguration enabled =
new ServerConfiguration("", "", true, null, Collections.emptyMap());
FeatureFlaggingGateway.dispatch(enabled);
assertTrue(FeatureFlaggingGateway.isObserveFullEvaluationDataEnabled());

final ServerConfiguration disabled =
new ServerConfiguration("", "", false, null, Collections.emptyMap());
FeatureFlaggingGateway.dispatch(disabled);
assertFalse(FeatureFlaggingGateway.isObserveFullEvaluationDataEnabled());
}

private static void clearCurrentServerConfiguration() {
FeatureFlaggingGateway.dispatch((ServerConfiguration) null);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package datadog.trace.api.featureflag.flagevaluation;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
Expand Down Expand Up @@ -70,4 +71,23 @@ void defaultsNullLazyContextAttributes() {

assertTrue(event.contextAttributes().isEmpty());
}

@Test
void observeFullEvaluationDataDefaultsToFalseOnConvenienceConstructors() {
final Map<String, Object> attrs = Collections.emptyMap();
assertFalse(new FlagEvalEvent("f", "on", "a", "t", 1L, attrs).observeFullEvaluationData);
assertFalse(new FlagEvalEvent("f", "on", "a", "t", null, 1L, attrs).observeFullEvaluationData);
assertFalse(
new FlagEvalEvent("f", "on", "a", "t", null, 1L, () -> attrs).observeFullEvaluationData);
}

@Test
void storesExplicitObserveFullEvaluationData() {
final Map<String, Object> attrs = Collections.emptyMap();
assertTrue(
new FlagEvalEvent("f", "on", "a", "t", null, 1L, true, attrs).observeFullEvaluationData);
assertTrue(
new FlagEvalEvent("f", "on", "a", "t", null, 1L, true, () -> attrs)
.observeFullEvaluationData);
}
}
Loading
Loading