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
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ class DDEvaluator implements Evaluator, FeatureFlaggingGateway.ConfigListener {
static final String METADATA_SPLIT_SERIAL_ID = "__dd_split_serial_id";
static final String METADATA_DO_LOG = "__dd_do_log";

// Stamped on every DD-produced evaluation (including PROVIDER_NOT_READY, with false). Missing
// key = non-DD provider; the hook falls back to false (fail-closed).
static final String METADATA_OBSERVE_FULL_EVALUATION_DATA = "observe_full_evaluation_data";

// Read once: when off, the __dd_* span-enrichment metadata is not attached to evaluations, so an
// enabled provider pays nothing extra unless span enrichment is also enabled. The gate does not
// change at runtime, and this class is loaded lazily (well after startup) so config is ready.
Expand Down Expand Up @@ -104,30 +108,39 @@ public <T> ProviderEvaluation<T> evaluate(
final String key,
final T defaultValue,
final EvaluationContext context) {
// Snapshot the config once and thread observeFullEvaluationData through every
// ProviderEvaluation returned, so the hook's consent decision is pinned to this evaluation's
// config and cannot drift on a concurrent Remote Config swap.
final ServerConfiguration config = configuration.get();
final boolean observeFullEvaluationData = config != null && config.observeFullEvaluationData;
try {
final ServerConfiguration config = configuration.get();
if (config == null) {
return error(defaultValue, ErrorCode.PROVIDER_NOT_READY);
return error(defaultValue, ErrorCode.PROVIDER_NOT_READY, null, observeFullEvaluationData);
}

if (context == null) {
return error(defaultValue, ErrorCode.INVALID_CONTEXT);
return error(defaultValue, ErrorCode.INVALID_CONTEXT, null, observeFullEvaluationData);
}

final Flag flag = config.flags.get(key);
if (flag == null) {
return error(defaultValue, ErrorCode.FLAG_NOT_FOUND);
return error(defaultValue, ErrorCode.FLAG_NOT_FOUND, null, observeFullEvaluationData);
}

if (!flag.enabled) {
return ProviderEvaluation.<T>builder()
.value(defaultValue)
.reason(Reason.DISABLED.name())
.flagMetadata(consentMetadata(observeFullEvaluationData))
.build();
}

if (flag.allocations == null) {
return error(defaultValue, ErrorCode.GENERAL, "Missing allocations for flag " + key);
return error(
defaultValue,
ErrorCode.GENERAL,
"Missing allocations for flag " + key,
observeFullEvaluationData);
}

final Date now = new Date();
Expand All @@ -148,10 +161,19 @@ public <T> ProviderEvaluation<T> evaluate(
for (final Split split : allocation.splits) {
if (isEmpty(split.shards)) {
return resolveVariant(
target, key, defaultValue, flag, split.variationKey, allocation, split, context);
target,
key,
defaultValue,
flag,
split.variationKey,
allocation,
split,
context,
observeFullEvaluationData);
} else {
if (targetingKey == null) {
return error(defaultValue, ErrorCode.TARGETING_KEY_MISSING);
return error(
defaultValue, ErrorCode.TARGETING_KEY_MISSING, null, observeFullEvaluationData);
}
// To match a split, subject must match ALL underlying shards
boolean allShardsMatch = true;
Expand All @@ -170,7 +192,8 @@ public <T> ProviderEvaluation<T> evaluate(
split.variationKey,
allocation,
split,
context);
context,
observeFullEvaluationData);
}
}
}
Expand All @@ -180,32 +203,35 @@ public <T> ProviderEvaluation<T> evaluate(
return ProviderEvaluation.<T>builder()
.value(defaultValue)
.reason(Reason.DEFAULT.name())
.flagMetadata(consentMetadata(observeFullEvaluationData))
.build();
} catch (final PatternSyntaxException e) {
return error(defaultValue, ErrorCode.PARSE_ERROR, e);
return error(defaultValue, ErrorCode.PARSE_ERROR, e.getMessage(), observeFullEvaluationData);
} catch (final NumberFormatException e) {
return error(defaultValue, ErrorCode.TYPE_MISMATCH, e);
return error(
defaultValue, ErrorCode.TYPE_MISMATCH, e.getMessage(), observeFullEvaluationData);
} catch (final Exception e) {
return error(defaultValue, ErrorCode.GENERAL, e);
return error(defaultValue, ErrorCode.GENERAL, e.getMessage(), observeFullEvaluationData);
}
}

private static <T> ProviderEvaluation<T> error(final T defaultValue, final ErrorCode code) {
return error(defaultValue, code, (String) null);
}

private static <T> ProviderEvaluation<T> error(
final T defaultValue, final ErrorCode code, final Throwable cause) {
return error(defaultValue, code, cause == null ? null : cause.getMessage());
private static ImmutableMetadata consentMetadata(final boolean observeFullEvaluationData) {
return ImmutableMetadata.builder()
.addBoolean(METADATA_OBSERVE_FULL_EVALUATION_DATA, observeFullEvaluationData)
.build();
}

private static <T> ProviderEvaluation<T> error(
final T defaultValue, final ErrorCode code, final String errorMessage) {
final T defaultValue,
final ErrorCode code,
final String errorMessage,
final boolean observeFullEvaluationData) {
return ProviderEvaluation.<T>builder()
.value(defaultValue)
.reason(Reason.ERROR.name())
.errorCode(code)
.errorMessage(errorMessage)
.flagMetadata(consentMetadata(observeFullEvaluationData))
.build();
}

Expand Down Expand Up @@ -367,14 +393,16 @@ private static <T> ProviderEvaluation<T> resolveVariant(
final String variationKey,
final Allocation allocation,
final Split split,
final EvaluationContext context) {
final EvaluationContext context,
final boolean observeFullEvaluationData) {
final Variant variant = flag.variations.get(variationKey);
if (variant == null) {
return ProviderEvaluation.<T>builder()
.value(defaultValue)
.reason(Reason.ERROR.name())
.errorCode(ErrorCode.GENERAL)
.errorMessage("Variant not found for: " + variationKey)
.flagMetadata(consentMetadata(observeFullEvaluationData))
.build();
}

Expand All @@ -385,7 +413,8 @@ private static <T> ProviderEvaluation<T> resolveVariant(
"Requested type "
+ target.getSimpleName()
+ " does not match flag variationType "
+ flag.variationType.name());
+ flag.variationType.name(),
observeFullEvaluationData);
}

final T mappedValue;
Expand All @@ -400,7 +429,8 @@ private static <T> ProviderEvaluation<T> resolveVariant(
+ "' value does not match declared type "
+ flag.variationType.name()
+ ": "
+ e.getMessage());
+ e.getMessage(),
observeFullEvaluationData);
}

// Stamp eval-time at the resolution point so first/last_evaluation reflect evaluation time,
Expand All @@ -411,7 +441,8 @@ private static <T> ProviderEvaluation<T> resolveVariant(
.addString("flagKey", flag.key)
.addString("variationType", flag.variationType.name())
.addString("allocationKey", allocation.key)
.addLong("dd.eval.timestamp_ms", evalTimestampMs);
.addLong("dd.eval.timestamp_ms", evalTimestampMs)
.addBoolean(METADATA_OBSERVE_FULL_EVALUATION_DATA, observeFullEvaluationData);
// Surface the UFC split's serial id and the allocation's doLog flag for APM span enrichment —
// only when span enrichment is on, so a provider without enrichment pays nothing extra.
// __dd_split_serial_id is omitted when the split carries no serial id; __dd_do_log is always
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,17 +107,41 @@ public void finallyAfter(
// targetingKey from evaluation context
final String targetingKey =
ctx != null && ctx.getCtx() != null ? ctx.getCtx().getTargetingKey() : null;
final Map<String, Value> attrs = snapshotAttrs(ctx);

w.enqueue(
new FlagEvalEvent(
flagKey,
variant,
allocationKey,
targetingKey,
errorMessage,
evalTimeMs,
() -> extractAttrs(attrs)));

// Consent is read from metadata stamped by DDEvaluator (pinned to its ServerConfiguration).
// Missing key = non-DD provider → false, the privacy-preserving default.
final Boolean consentFromMetadata =
metadata != null
? metadata.getBoolean(DDEvaluator.METADATA_OBSERVE_FULL_EVALUATION_DATA)
: null;
final boolean observeFullEvaluationData = consentFromMetadata != null && consentFromMetadata;

// On the protected path the evaluation context is dropped on emit and never consulted by
// the aggregator, so skip the snapshot + supplier allocation entirely.
if (observeFullEvaluationData) {
final Map<String, Value> attrs = snapshotAttrs(ctx);
w.enqueue(
new FlagEvalEvent(
flagKey,
variant,
allocationKey,
targetingKey,
errorMessage,
evalTimeMs,
true,
() -> extractAttrs(attrs)));
} else {
w.enqueue(
new FlagEvalEvent(
flagKey,
variant,
allocationKey,
targetingKey,
errorMessage,
evalTimeMs,
false,
Collections.<String, Object>emptyMap()));
}
} 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 @@ -198,7 +198,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 All @@ -213,6 +213,37 @@ public void testNoAllocations() {
assertThat(details.getErrorCode(), nullValue());
}

// ---- observeFullEvaluationData metadata is stamped from the evaluator's ServerConfiguration
// ----

@Test
public void observeFullEvaluationDataStampedFromEvaluatorConfigOnSuccess() {
final Map<String, Flag> flags = new HashMap<>();
flags.put("null-allocation", new Flag("target", true, null, null, null));
final DDEvaluator evaluator = new DDEvaluator(mock(Runnable.class));
evaluator.accept(new ServerConfiguration("", "", true, null, flags));

final EvaluationContext ctx = new MutableContext("target").setTargetingKey("k");
final ProviderEvaluation<?> details =
evaluator.evaluate(Integer.class, "unknown-flag", 23, ctx);

assertThat(details.getErrorCode(), equalTo(ErrorCode.FLAG_NOT_FOUND));
assertThat(
details.getFlagMetadata().getBoolean(DDEvaluator.METADATA_OBSERVE_FULL_EVALUATION_DATA),
equalTo(true));
}

@Test
public void observeFullEvaluationDataDefaultsToFalseWhenEvaluatorHasNoConfig() {
final DDEvaluator evaluator = new DDEvaluator(mock(Runnable.class));
final ProviderEvaluation<?> details =
evaluator.evaluate(Integer.class, "test", 23, mock(EvaluationContext.class));
assertThat(details.getErrorCode(), equalTo(ErrorCode.PROVIDER_NOT_READY));
assertThat(
details.getFlagMetadata().getBoolean(DDEvaluator.METADATA_OBSERVE_FULL_EVALUATION_DATA),
equalTo(false));
}

private static Arguments[] flatteningTestCases() {
final List<Arguments> arguments = new ArrayList<>();
arguments.add(Arguments.of(emptyMap(), emptyMap()));
Expand Down
Loading