diff --git a/lib/sdk/server/build.gradle b/lib/sdk/server/build.gradle
index 399c78df..4a2c1537 100644
--- a/lib/sdk/server/build.gradle
+++ b/lib/sdk/server/build.gradle
@@ -71,7 +71,7 @@ ext.versions = [
"guava": "32.0.1-jre",
"jackson": "2.11.2",
"launchdarklyJavaSdkCommon": "2.6.0",
- "launchdarklyJavaSdkInternal": "1.11.1",
+ "launchdarklyJavaSdkInternal": "1.13.0",
"launchdarklyLogging": "1.1.0",
"okhttp": "4.12.0", // specify this for the SDK build instead of relying on the transitive dependency from okhttp-eventsource
"okhttpEventsource": "5.0.0",
diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/DefaultEventProcessorWrapper.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/DefaultEventProcessorWrapper.java
index 472c4bb6..aa25bf29 100644
--- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/DefaultEventProcessorWrapper.java
+++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/DefaultEventProcessorWrapper.java
@@ -34,6 +34,15 @@ public void recordEvaluationEvent(LDContext context, String flagKey, int flagVer
LDValue value, EvaluationReason reason, LDValue defaultValue, String prerequisiteOfFlagKey,
boolean requireFullEvent, Long debugEventsUntilDate, boolean excludeFromSummaries,
Long samplingRatio) {
+ recordEvaluationEvent(context, flagKey, flagVersion, variation, value, reason, defaultValue, prerequisiteOfFlagKey,
+ requireFullEvent, debugEventsUntilDate, excludeFromSummaries, samplingRatio, false);
+ }
+
+ @Override
+ public void recordEvaluationEvent(LDContext context, String flagKey, int flagVersion, int variation,
+ LDValue value, EvaluationReason reason, LDValue defaultValue, String prerequisiteOfFlagKey,
+ boolean requireFullEvent, Long debugEventsUntilDate, boolean excludeFromSummaries,
+ Long samplingRatio, boolean overrideAffected) {
eventProcessor.sendEvent(new Event.FeatureRequest(
System.currentTimeMillis(),
flagKey,
@@ -48,7 +57,8 @@ public void recordEvaluationEvent(LDContext context, String flagKey, int flagVer
debugEventsUntilDate,
false,
samplingRatio != null ? samplingRatio : 1,
- excludeFromSummaries
+ excludeFromSummaries,
+ overrideAffected
));
}
diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/EvaluationRecorder.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/EvaluationRecorder.java
index 8dfd1005..75a16342 100644
--- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/EvaluationRecorder.java
+++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/EvaluationRecorder.java
@@ -16,7 +16,8 @@ default void recordEvaluation(DataModel.FeatureFlag flag, LDContext context, Eva
default void recordPrerequisiteEvaluation(DataModel.FeatureFlag flag, DataModel.FeatureFlag prereqOfFlag, LDContext context, EvalResult result) {
// default is no op
}
- default void recordEvaluationError(DataModel.FeatureFlag flag, LDContext context, LDValue defaultValue, EvaluationReason.ErrorKind errorKind) {
+ default void recordEvaluationError(DataModel.FeatureFlag flag, LDContext context, LDValue defaultValue,
+ EvaluationReason.ErrorKind errorKind, boolean overrideAffected) {
// default is no op
}
default void recordEvaluationUnknownFlagError(String flagKey, LDContext context, LDValue defaultValue, EvaluationReason.ErrorKind errorKind) {
diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/InputValidatingEvaluator.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/InputValidatingEvaluator.java
index bfebfb98..e08c2a5d 100644
--- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/InputValidatingEvaluator.java
+++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/InputValidatingEvaluator.java
@@ -144,7 +144,8 @@ EvalResultAndFlag evaluate(String flagKey, LDContext context, LDValue defaultVal
!value.isNull() &&
value.getType() != requireType) {
logger.error("Feature flag \"{}\"; evaluation expected result as {}, but got {}", flagKey, defaultValue.getType(), value.getType());
- recorder.recordEvaluationError(featureFlag, context, defaultValue, ErrorKind.WRONG_TYPE);
+ recorder.recordEvaluationError(featureFlag, context, defaultValue, ErrorKind.WRONG_TYPE,
+ result.isOverrideAffected());
// The type mismatch replaces the reason. The evaluation read the same definitions, so the
// new reason keeps the override-affected marking.
return new EvalResultAndFlag(
@@ -163,7 +164,8 @@ EvalResultAndFlag evaluate(String flagKey, LDContext context, LDValue defaultVal
if (featureFlag == null) {
recorder.recordEvaluationUnknownFlagError(flagKey, context, defaultValue, ErrorKind.EXCEPTION);
} else {
- recorder.recordEvaluationError(featureFlag, context, defaultValue, ErrorKind.EXCEPTION);
+ // The evaluation did not complete, so nothing is known about which definitions it read.
+ recorder.recordEvaluationError(featureFlag, context, defaultValue, ErrorKind.EXCEPTION, false);
}
return new EvalResultAndFlag(EvalResult.of(defaultValue, NO_VARIATION, EvaluationReason.exception(e)), null);
}
@@ -255,6 +257,8 @@ private static DataModel.Segment getSegment(ReadOnlyStore store, String key) {
*/
private static EvaluationRecorder makeEvaluationRecorder(EventProcessor processor, boolean withReasons) {
return new EvaluationRecorder() {
+ // The marking passed to the event processor is the evaluation result's own record of the
+ // definitions it read. Event generation does not read the evaluation reason.
@Override
public void recordEvaluation(FeatureFlag flag, LDContext context, EvalResult result, LDValue defaultValue) {
processor.recordEvaluationEvent(
@@ -269,10 +273,13 @@ public void recordEvaluation(FeatureFlag flag, LDContext context, EvalResult res
flag.isTrackEvents() || result.isForceReasonTracking(),
flag.getDebugEventsUntilDate(),
flag.isExcludeFromSummaries(),
- flag.getSamplingRatio()
+ flag.getSamplingRatio(),
+ result.isOverrideAffected()
);
}
+ // A prerequisite record carries the prerequisite's own marking, which its own subtree of
+ // reads set. It is not the marking of the evaluation that requested it.
@Override
public void recordPrerequisiteEvaluation(FeatureFlag flag, FeatureFlag prereqOfFlag, LDContext context, EvalResult result) {
processor.recordEvaluationEvent(
@@ -287,25 +294,28 @@ public void recordPrerequisiteEvaluation(FeatureFlag flag, FeatureFlag prereqOfF
flag.isTrackEvents() || result.isForceReasonTracking(),
flag.getDebugEventsUntilDate(),
flag.isExcludeFromSummaries(),
- flag.getSamplingRatio()
+ flag.getSamplingRatio(),
+ result.isOverrideAffected()
);
}
@Override
- public void recordEvaluationError(FeatureFlag flag, LDContext context, LDValue defaultValue, ErrorKind errorKind) {
+ public void recordEvaluationError(FeatureFlag flag, LDContext context, LDValue defaultValue, ErrorKind errorKind,
+ boolean overrideAffected) {
processor.recordEvaluationEvent(
context,
flag.getKey(),
flag.getVersion(),
NO_VARIATION,
defaultValue,
- withReasons ? EvaluationReason.error(errorKind) : null,
+ withReasons ? EvaluationReason.error(errorKind).withOverrideAffected(overrideAffected) : null,
defaultValue,
null,
flag.isTrackEvents(),
flag.getDebugEventsUntilDate(),
flag.isExcludeFromSummaries(),
- flag.getSamplingRatio()
+ flag.getSamplingRatio(),
+ overrideAffected
);
}
diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/NoOpEventProcessor.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/NoOpEventProcessor.java
index 60684967..37b08ce4 100644
--- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/NoOpEventProcessor.java
+++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/NoOpEventProcessor.java
@@ -20,6 +20,14 @@ public void recordEvaluationEvent(LDContext context, String flagKey, int flagVer
// no-op
}
+ @Override
+ public void recordEvaluationEvent(LDContext context, String flagKey, int flagVersion, int variation, LDValue value,
+ EvaluationReason reason, LDValue defaultValue, String prerequisiteOfFlagKey,
+ boolean requireFullEvent, Long debugEventsUntilDate, boolean excludeFromSummaries,
+ Long samplingRatio, boolean overrideAffected) {
+ // no-op
+ }
+
@Override
public void recordIdentifyEvent(LDContext context) {
// no-op
diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/subsystems/EventProcessor.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/subsystems/EventProcessor.java
index 2381b80f..3fb3a3c2 100644
--- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/subsystems/EventProcessor.java
+++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/subsystems/EventProcessor.java
@@ -57,6 +57,54 @@ void recordEvaluationEvent(
boolean excludeFromSummaries,
Long samplingRatio
);
+
+ /**
+ * Records the action of evaluating a feature flag, with the override-affected marking.
+ *
+ * An override-affected evaluation is one that read at least one definition from the SDK's
+ * override store: the flag itself, a prerequisite at any depth, or a segment consulted during
+ * matching. Such an evaluation produces no individual feature event and no debug event, whatever
+ * the flag's configuration requests, and it is counted in a separate summary counter that
+ * carries the marker. The SDK keys this behavior on the marking alone, not on the reason.
+ *
+ * The default implementation ignores the marking and calls the method without it, so an
+ * existing custom implementation keeps working. Flag overrides are currently experimental and
+ * subject to change.
+ *
+ * @param context the evaluation context
+ * @param flagKey key of the feature flag that was evaluated
+ * @param flagVersion the version of the flag, or {@link #NO_VERSION} if the flag was not found
+ * @param variation the result variation index, or {@link EvaluationDetail#NO_VARIATION} if evaluation failed
+ * @param value the result value
+ * @param reason the evaluation reason, or null if the reason was not requested
+ * @param defaultValue the default value parameter for the evaluation
+ * @param prerequisiteOfFlagKey the key of the flag that this flag was evaluated as a prerequisite of,
+ * or null if this flag was evaluated for itself
+ * @param requireFullEvent true if full-fidelity analytics events should be sent for this flag
+ * @param debugEventsUntilDate if non-null, debug events are to be generated until this millisecond time
+ * @param excludeFromSummaries true if the event evaluation should not be included in summaries
+ * @param samplingRatio ratio used to control event sampling
+ * @param overrideAffected true if an override affected the evaluation
+ * @since 7.18.0
+ */
+ default void recordEvaluationEvent(
+ LDContext context,
+ String flagKey,
+ int flagVersion,
+ int variation,
+ LDValue value,
+ EvaluationReason reason,
+ LDValue defaultValue,
+ String prerequisiteOfFlagKey,
+ boolean requireFullEvent,
+ Long debugEventsUntilDate,
+ boolean excludeFromSummaries,
+ Long samplingRatio,
+ boolean overrideAffected
+ ) {
+ recordEvaluationEvent(context, flagKey, flagVersion, variation, value, reason, defaultValue,
+ prerequisiteOfFlagKey, requireFullEvent, debugEventsUntilDate, excludeFromSummaries, samplingRatio);
+ }
/**
* Registers an evaluation context, as when the SDK's {@code identify} method is called.
diff --git a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/LDClientOverrideEventsTest.java b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/LDClientOverrideEventsTest.java
new file mode 100644
index 00000000..a56bf9ae
--- /dev/null
+++ b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/LDClientOverrideEventsTest.java
@@ -0,0 +1,271 @@
+package com.launchdarkly.sdk.server;
+
+import com.launchdarkly.logging.LDLogLevel;
+import com.launchdarkly.sdk.EvaluationReason;
+import com.launchdarkly.sdk.EvaluationReason.ErrorKind;
+import com.launchdarkly.sdk.LDContext;
+import com.launchdarkly.sdk.LDValue;
+import com.launchdarkly.sdk.internal.events.Event;
+import com.launchdarkly.sdk.server.DataModel.FeatureFlag;
+import com.launchdarkly.sdk.server.DataModel.Segment;
+import com.launchdarkly.sdk.server.TestComponents.TestEventProcessor;
+import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.DataKind;
+import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ItemDescriptor;
+import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.KeyedItems;
+import com.launchdarkly.sdk.server.subsystems.EventSender;
+
+import org.junit.Test;
+
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+import static com.launchdarkly.sdk.server.ModelBuilders.clauseMatchingSegment;
+import static com.launchdarkly.sdk.server.ModelBuilders.flagBuilder;
+import static com.launchdarkly.sdk.server.ModelBuilders.prerequisite;
+import static com.launchdarkly.sdk.server.ModelBuilders.ruleBuilder;
+import static com.launchdarkly.sdk.server.ModelBuilders.segmentBuilder;
+import static com.launchdarkly.sdk.server.OverrideLayerTest.collections;
+import static com.launchdarkly.sdk.server.OverrideLayerTest.flagsOnly;
+import static com.launchdarkly.sdk.server.OverrideTestDataSources.initializerWith;
+import static com.launchdarkly.sdk.server.TestComponents.specificComponent;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * The override-affected marking as the client hands it to the event processor, and the resulting
+ * analytics output: no individual feature or debug event for a marked evaluation, and a summary
+ * counter that carries the marker.
+ */
+@SuppressWarnings("javadoc")
+public class LDClientOverrideEventsTest extends BaseTest {
+ private static final String SDK_KEY = "sdk-key";
+ private static final LDContext CONTEXT = LDContext.create("user-key");
+ private static final LDValue DEFAULT = LDValue.of("default");
+
+ private static FeatureFlag valueFlag(String key, int version, String value) {
+ return flagBuilder(key).version(version).on(false).offVariation(0).variations(LDValue.of(value)).build();
+ }
+
+ private LDConfig.Builder baseBuilder(Iterable>> ldData,
+ TestOverrideSource source) {
+ return new LDConfig.Builder()
+ .dataSystem(Components.dataSystem().custom().initializers(initializerWith(ldData)).overrides(source))
+ .logging(Components.logging(testLogging).level(LDLogLevel.DEBUG));
+ }
+
+ private static List featureRecords(TestEventProcessor events, String flagKey) {
+ List result = new ArrayList<>();
+ for (Event e : events.events) {
+ if (e instanceof Event.FeatureRequest && ((Event.FeatureRequest) e).getKey().equals(flagKey)) {
+ result.add((Event.FeatureRequest) e);
+ }
+ }
+ return result;
+ }
+
+ private static Event.FeatureRequest singleRecord(TestEventProcessor events, String flagKey) {
+ List records = featureRecords(events, flagKey);
+ assertEquals("expected one record for " + flagKey, 1, records.size());
+ return records.get(0);
+ }
+
+ @Test
+ public void evaluationOfOverriddenFlagIsRecordedAsOverrideAffected() throws Exception {
+ TestEventProcessor events = new TestEventProcessor();
+ TestOverrideSource source = new TestOverrideSource(flagsOnly(valueFlag("overridden", 5, "override-value")));
+ Iterable>> ldData = flagsOnly(
+ valueFlag("overridden", 1, "ld-value"), valueFlag("plain", 1, "plain-value"));
+
+ try (LDClient client = new LDClient(SDK_KEY, baseBuilder(ldData, source).events(specificComponent(events)).build())) {
+ client.stringVariation("overridden", CONTEXT, "default");
+ client.stringVariation("plain", CONTEXT, "default");
+
+ Event.FeatureRequest overridden = singleRecord(events, "overridden");
+ assertTrue(overridden.isOverrideAffected());
+ assertEquals(5, overridden.getVersion());
+ assertEquals(LDValue.of("override-value"), overridden.getValue());
+
+ Event.FeatureRequest plain = singleRecord(events, "plain");
+ assertFalse(plain.isOverrideAffected());
+ }
+ }
+
+ @Test
+ public void recordedMarkingDoesNotDependOnRequestingTheReason() throws Exception {
+ TestEventProcessor events = new TestEventProcessor();
+ TestOverrideSource source = new TestOverrideSource(flagsOnly(valueFlag("overridden", 5, "override-value")));
+
+ try (LDClient client = new LDClient(SDK_KEY, baseBuilder(flagsOnly(), source).events(specificComponent(events)).build())) {
+ client.stringVariation("overridden", CONTEXT, "default");
+ Event.FeatureRequest record = singleRecord(events, "overridden");
+ assertTrue(record.isOverrideAffected());
+ assertNull(record.getReason());
+ }
+ }
+
+ @Test
+ public void prerequisiteRecordsCarryTheirOwnMarking() throws Exception {
+ TestEventProcessor events = new TestEventProcessor();
+ FeatureFlag parent = flagBuilder("parent").version(1).on(true).fallthroughVariation(1).offVariation(0)
+ .variations(LDValue.of("off"), LDValue.of("on"))
+ .prerequisites(prerequisite("overridden-prereq", 1), prerequisite("plain-prereq", 1)).build();
+ FeatureFlag ldOverriddenPrereq = flagBuilder("overridden-prereq").version(1).on(false).offVariation(0)
+ .variations(LDValue.of("a"), LDValue.of("b")).build();
+ FeatureFlag plainPrereq = flagBuilder("plain-prereq").version(1).on(true).fallthroughVariation(1).offVariation(0)
+ .variations(LDValue.of("a"), LDValue.of("b")).build();
+ FeatureFlag overriddenPrereq = flagBuilder("overridden-prereq").version(2).on(true).fallthroughVariation(1)
+ .offVariation(0).variations(LDValue.of("a"), LDValue.of("b")).build();
+ TestOverrideSource source = new TestOverrideSource(flagsOnly(overriddenPrereq));
+
+ try (LDClient client = new LDClient(SDK_KEY,
+ baseBuilder(flagsOnly(parent, ldOverriddenPrereq, plainPrereq), source).events(specificComponent(events)).build())) {
+ assertEquals("on", client.stringVariation("parent", CONTEXT, "default"));
+
+ assertTrue(singleRecord(events, "parent").isOverrideAffected());
+ Event.FeatureRequest overriddenRecord = singleRecord(events, "overridden-prereq");
+ assertTrue(overriddenRecord.isOverrideAffected());
+ assertEquals("parent", overriddenRecord.getPrereqOf());
+ assertEquals(2, overriddenRecord.getVersion());
+ Event.FeatureRequest plainRecord = singleRecord(events, "plain-prereq");
+ assertFalse(plainRecord.isOverrideAffected());
+ assertEquals("parent", plainRecord.getPrereqOf());
+ }
+ }
+
+ @Test
+ public void segmentReadMarksTheRecord() throws Exception {
+ TestEventProcessor events = new TestEventProcessor();
+ FeatureFlag usesSegment = flagBuilder("uses-segment").version(1).on(true).fallthroughVariation(0).offVariation(0)
+ .variations(LDValue.of("no"), LDValue.of("yes"))
+ .rules(ruleBuilder().id("r").variation(1).clauses(clauseMatchingSegment("seg")).build()).build();
+ Segment ldSegment = segmentBuilder("seg").version(1).build();
+ Segment overrideSegment = segmentBuilder("seg").version(2).build(); // still does not include the context
+ TestOverrideSource source = new TestOverrideSource(
+ collections(Collections.emptyList(), Collections.singletonList(overrideSegment)));
+
+ try (LDClient client = new LDClient(SDK_KEY, baseBuilder(
+ collections(Collections.singletonList(usesSegment), Collections.singletonList(ldSegment)), source)
+ .events(specificComponent(events)).build())) {
+ assertEquals("no", client.stringVariation("uses-segment", CONTEXT, "default"));
+ assertTrue(singleRecord(events, "uses-segment").isOverrideAffected());
+ }
+ }
+
+ @Test
+ public void typeMismatchRecordKeepsMarking() throws Exception {
+ TestEventProcessor events = new TestEventProcessor();
+ TestOverrideSource source = new TestOverrideSource(flagsOnly(valueFlag("overridden", 5, "a string")));
+
+ try (LDClient client = new LDClient(SDK_KEY, baseBuilder(flagsOnly(), source).events(specificComponent(events)).build())) {
+ assertTrue(client.boolVariation("overridden", CONTEXT, true));
+ Event.FeatureRequest record = singleRecord(events, "overridden");
+ assertTrue(record.isOverrideAffected());
+ assertEquals(EvaluationReason.error(ErrorKind.WRONG_TYPE).withOverrideAffected(true),
+ client.boolVariationDetail("overridden", CONTEXT, true).getReason());
+ }
+ }
+
+ @Test
+ public void unknownFlagRecordIsNotMarked() throws Exception {
+ TestEventProcessor events = new TestEventProcessor();
+ TestOverrideSource source = new TestOverrideSource(flagsOnly(valueFlag("overridden", 5, "v")));
+ try (LDClient client = new LDClient(SDK_KEY, baseBuilder(flagsOnly(), source).events(specificComponent(events)).build())) {
+ client.stringVariation("missing", CONTEXT, "default");
+ assertFalse(singleRecord(events, "missing").isOverrideAffected());
+ }
+ }
+
+ /**
+ * Captures the analytics payloads that the default event processor sends.
+ */
+ private static final class CapturingEventSender implements EventSender {
+ final BlockingQueue payloads = new LinkedBlockingQueue<>();
+
+ @Override
+ public Result sendAnalyticsEvents(byte[] data, int eventCount, URI eventsBaseUri) {
+ payloads.add(LDValue.parse(new String(data, StandardCharsets.UTF_8)));
+ return Result.SUCCESS;
+ }
+
+ @Override
+ public Result sendDiagnosticEvent(byte[] data, URI eventsBaseUri) {
+ return Result.SUCCESS;
+ }
+
+ @Override
+ public void close() {
+ }
+ }
+
+ private static List eventsOfKind(LDValue payload, String kind) {
+ List result = new ArrayList<>();
+ for (LDValue e : payload.values()) {
+ if (e.get("kind").stringValue().equals(kind)) {
+ result.add(e);
+ }
+ }
+ return result;
+ }
+
+ @Test
+ public void overrideAffectedEvaluationsAppearOnlyInSummaryOutput() throws Exception {
+ long debugUntil = System.currentTimeMillis() + 100000;
+ // Both flags request individual feature events and debug events.
+ FeatureFlag overridden = flagBuilder("overridden").version(300).on(false).offVariation(0)
+ .variations(LDValue.of("override-value")).trackEvents(true).debugEventsUntilDate(debugUntil).build();
+ FeatureFlag plain = flagBuilder("plain").version(100).on(false).offVariation(0)
+ .variations(LDValue.of("plain-value")).trackEvents(true).debugEventsUntilDate(debugUntil).build();
+ TestOverrideSource source = new TestOverrideSource(flagsOnly(overridden));
+ CapturingEventSender sender = new CapturingEventSender();
+
+ LDConfig config = baseBuilder(flagsOnly(plain), source)
+ .events(Components.sendEvents().eventSender(specificComponent(sender)).flushInterval(Duration.ofHours(1)))
+ .diagnosticOptOut(true)
+ .build();
+
+ try (LDClient client = new LDClient(SDK_KEY, config)) {
+ client.stringVariation("overridden", CONTEXT, "default");
+ client.stringVariation("overridden", CONTEXT, "default");
+ client.stringVariation("plain", CONTEXT, "default");
+ client.flush();
+
+ LDValue payload = sender.payloads.poll(5, TimeUnit.SECONDS);
+ assertNotNull("no analytics payload was sent", payload);
+
+ // The overridden flag produced no feature event and no debug event. The plain flag produced both.
+ List featureEvents = eventsOfKind(payload, "feature");
+ List debugEvents = eventsOfKind(payload, "debug");
+ assertEquals(1, featureEvents.size());
+ assertEquals("plain", featureEvents.get(0).get("key").stringValue());
+ assertEquals(1, debugEvents.size());
+ assertEquals("plain", debugEvents.get(0).get("key").stringValue());
+ assertEquals(1, eventsOfKind(payload, "index").size());
+
+ // Both flags are summarized. Only the overridden flag's counter carries the marker.
+ List summaries = eventsOfKind(payload, "summary");
+ assertEquals(1, summaries.size());
+ LDValue features = summaries.get(0).get("features");
+ LDValue overriddenCounters = features.get("overridden").get("counters");
+ assertEquals(1, overriddenCounters.size());
+ LDValue overriddenCounter = overriddenCounters.get(0);
+ assertTrue(overriddenCounter.get("overrideAffected").booleanValue());
+ assertEquals(300, overriddenCounter.get("version").intValue());
+ assertEquals(2, overriddenCounter.get("count").intValue());
+ assertEquals(LDValue.of("override-value"), overriddenCounter.get("value"));
+ LDValue plainCounter = features.get("plain").get("counters").get(0);
+ assertTrue(plainCounter.get("overrideAffected").isNull());
+ assertEquals(1, plainCounter.get("count").intValue());
+ }
+ }
+}
diff --git a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/OverrideSpecVectorsTest.java b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/OverrideSpecVectorsTest.java
index 467a362e..0e3b1f85 100644
--- a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/OverrideSpecVectorsTest.java
+++ b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/OverrideSpecVectorsTest.java
@@ -4,7 +4,9 @@
import com.launchdarkly.sdk.EvaluationDetail;
import com.launchdarkly.sdk.LDContext;
import com.launchdarkly.sdk.LDValue;
+import com.launchdarkly.sdk.internal.events.Event;
import com.launchdarkly.sdk.json.JsonSerialization;
+import com.launchdarkly.sdk.server.TestComponents.TestEventProcessor;
import com.launchdarkly.sdk.server.integrations.DataSystemBuilder;
import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.DataKind;
import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ItemDescriptor;
@@ -25,6 +27,7 @@
import static com.launchdarkly.sdk.server.DataModel.SEGMENTS;
import static com.launchdarkly.sdk.server.OverrideTestDataSources.hangingSynchronizer;
import static com.launchdarkly.sdk.server.OverrideTestDataSources.initializerWith;
+import static com.launchdarkly.sdk.server.TestComponents.specificComponent;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@@ -32,7 +35,8 @@
/**
* Runs the OVERRIDE specification's test vectors. Each vector sets up LaunchDarkly data, an
* override layer, and an initialization state, evaluates one flag through the full client stack,
- * and checks the value, the variation index, and the reason.
+ * and checks the value, the variation index, the reason, and the marking that the client hands to the
+ * event processor for the evaluation.
*/
@SuppressWarnings("javadoc")
public class OverrideSpecVectorsTest extends BaseTest {
@@ -104,8 +108,9 @@ private void runVector(LDValue vector) throws Exception {
overrides.get("flags"), overrides.get("flagValues"), overrides.get("segments")));
DataSystemBuilder dataSystem = Components.dataSystem().custom().overrides(source);
+ TestEventProcessor events = new TestEventProcessor();
LDConfig.Builder config = new LDConfig.Builder()
- .events(Components.noEvents())
+ .events(specificComponent(events))
.logging(Components.logging(testLogging).level(LDLogLevel.DEBUG));
if (initialized) {
dataSystem.initializers(initializerWith(collectionsFromJson(
@@ -135,6 +140,22 @@ private void runVector(LDValue vector) throws Exception {
assertEquals("variationIndex", expect.get("variationIndex").intValue(), detail.getVariationIndex());
}
assertVectorReason(expect.get("reason"), LDValue.parse(JsonSerialization.serialize(detail.getReason())));
+
+ // summaryOverrideAffected is the marking the client hands to the event processor for this
+ // evaluation. The event processor keys individual event suppression and the summary counter
+ // marker on that value, not on the reason.
+ if (!expect.get("summaryOverrideAffected").isNull()) {
+ List records = new ArrayList<>();
+ for (Event e : events.events) {
+ if (e instanceof Event.FeatureRequest
+ && ((Event.FeatureRequest) e).getKey().equals(evaluate.get("flagKey").stringValue())) {
+ records.add((Event.FeatureRequest) e);
+ }
+ }
+ assertEquals("expected exactly one evaluation record for the flag", 1, records.size());
+ assertEquals("summaryOverrideAffected", expect.get("summaryOverrideAffected").booleanValue(),
+ records.get(0).isOverrideAffected());
+ }
}
}
diff --git a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/TestComponents.java b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/TestComponents.java
index 77b4d699..5bc8d51a 100644
--- a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/TestComponents.java
+++ b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/TestComponents.java
@@ -149,8 +149,17 @@ public void recordEvaluationEvent(LDContext context, String flagKey, int flagVer
EvaluationReason reason, LDValue defaultValue, String prerequisiteOfFlagKey, boolean requireFullEvent,
Long debugEventsUntilDate, boolean excludeFromSummaries,
Long samplingRatio) {
+ recordEvaluationEvent(context, flagKey, flagVersion, variation, value, reason, defaultValue, prerequisiteOfFlagKey,
+ requireFullEvent, debugEventsUntilDate, excludeFromSummaries, samplingRatio, false);
+ }
+
+ @Override
+ public void recordEvaluationEvent(LDContext context, String flagKey, int flagVersion, int variation, LDValue value,
+ EvaluationReason reason, LDValue defaultValue, String prerequisiteOfFlagKey, boolean requireFullEvent,
+ Long debugEventsUntilDate, boolean excludeFromSummaries, Long samplingRatio, boolean overrideAffected) {
events.add(new Event.FeatureRequest(System.currentTimeMillis(), flagKey, context, flagVersion,
- variation, value, defaultValue, reason, prerequisiteOfFlagKey, requireFullEvent, debugEventsUntilDate, false));
+ variation, value, defaultValue, reason, prerequisiteOfFlagKey, requireFullEvent, debugEventsUntilDate, false,
+ samplingRatio == null ? 1 : samplingRatio, excludeFromSummaries, overrideAffected));
}
@Override