diff --git a/lib/shared/internal/src/main/java/com/launchdarkly/sdk/internal/events/AggregatedEventSummarizer.java b/lib/shared/internal/src/main/java/com/launchdarkly/sdk/internal/events/AggregatedEventSummarizer.java
index 710c8937..62ca1d6a 100644
--- a/lib/shared/internal/src/main/java/com/launchdarkly/sdk/internal/events/AggregatedEventSummarizer.java
+++ b/lib/shared/internal/src/main/java/com/launchdarkly/sdk/internal/events/AggregatedEventSummarizer.java
@@ -31,9 +31,11 @@ public void summarizeEvent(
int variation,
LDValue value,
LDValue defaultValue,
- LDContext context
+ LDContext context,
+ boolean overrideAffected
) {
- summarizer.summarizeEvent(timestamp, flagKey, flagVersion, variation, value, defaultValue, context);
+ summarizer.summarizeEvent(timestamp, flagKey, flagVersion, variation, value, defaultValue, context,
+ overrideAffected);
}
@Override
diff --git a/lib/shared/internal/src/main/java/com/launchdarkly/sdk/internal/events/DefaultEventProcessor.java b/lib/shared/internal/src/main/java/com/launchdarkly/sdk/internal/events/DefaultEventProcessor.java
index f258243b..a8974370 100644
--- a/lib/shared/internal/src/main/java/com/launchdarkly/sdk/internal/events/DefaultEventProcessor.java
+++ b/lib/shared/internal/src/main/java/com/launchdarkly/sdk/internal/events/DefaultEventProcessor.java
@@ -544,8 +544,10 @@ private void processEvent(Event e, EventBuffer outbox) {
if(!fe.isExcludeFromSummaries()) {
outbox.addToSummary(fe);
}
- addFullEvent = fe.isTrackEvents();
- if (shouldDebugEvent(fe)) {
+ // An override-affected evaluation appears only in the summary counters. It produces no
+ // individual feature event and no debug event, whatever the flag's configuration requests.
+ addFullEvent = fe.isTrackEvents() && !fe.isOverrideAffected();
+ if (!fe.isOverrideAffected() && shouldDebugEvent(fe)) {
debugEvent = fe.toDebugEvent();
}
} else {
@@ -702,7 +704,8 @@ void addToSummary(Event.FeatureRequest e) {
e.getVariation(),
e.getValue(),
e.getDefaultVal(),
- e.getContext()
+ e.getContext(),
+ e.isOverrideAffected()
);
}
diff --git a/lib/shared/internal/src/main/java/com/launchdarkly/sdk/internal/events/Event.java b/lib/shared/internal/src/main/java/com/launchdarkly/sdk/internal/events/Event.java
index 16c5b666..49abe8e2 100644
--- a/lib/shared/internal/src/main/java/com/launchdarkly/sdk/internal/events/Event.java
+++ b/lib/shared/internal/src/main/java/com/launchdarkly/sdk/internal/events/Event.java
@@ -155,6 +155,7 @@ public static final class FeatureRequest extends Event {
private final boolean debug;
private final long samplingRatio;
private final boolean excludeFromSummaries;
+ private final boolean overrideAffected;
/**
* Constructs a feature request event.
@@ -171,12 +172,14 @@ public static final class FeatureRequest extends Event {
* @param trackEvents true if full event tracking is turned on for this flag
* @param debugEventsUntilDate if non-null, the time until which event debugging should be enabled
* @param debug true if this is a debugging event
- * @param excludeFromSummaries true to exclude the event from summaries
* @param samplingRatio the sampling ratio for the event
+ * @param excludeFromSummaries true to exclude the event from summaries
+ * @param overrideAffected true if an override affected the evaluation; see {@link #isOverrideAffected()}
*/
public FeatureRequest(long timestamp, String key, LDContext context, int version, int variation, LDValue value,
LDValue defaultVal, EvaluationReason reason, String prereqOf, boolean trackEvents,
- Long debugEventsUntilDate, boolean debug, long samplingRatio, boolean excludeFromSummaries) {
+ Long debugEventsUntilDate, boolean debug, long samplingRatio, boolean excludeFromSummaries,
+ boolean overrideAffected) {
super(timestamp, context);
this.key = key;
this.version = version;
@@ -190,6 +193,32 @@ public FeatureRequest(long timestamp, String key, LDContext context, int version
this.debug = debug;
this.excludeFromSummaries = excludeFromSummaries;
this.samplingRatio = samplingRatio;
+ this.overrideAffected = overrideAffected;
+ }
+
+ /**
+ * Constructs a feature request event that no override affected.
+ *
+ * @param timestamp the timestamp in milliseconds
+ * @param key the flag key
+ * @param context the context associated with the event
+ * @param version the flag version, or -1 if the flag was not found
+ * @param variation the result variation, or -1 if there was an error
+ * @param value the result value
+ * @param defaultVal the default value passed by the application
+ * @param reason the evaluation reason, if it is to be included in the event
+ * @param prereqOf if this flag was evaluated as a prerequisite, this is the key of the flag that referenced it
+ * @param trackEvents true if full event tracking is turned on for this flag
+ * @param debugEventsUntilDate if non-null, the time until which event debugging should be enabled
+ * @param debug true if this is a debugging event
+ * @param samplingRatio the sampling ratio for the event
+ * @param excludeFromSummaries true to exclude the event from summaries
+ */
+ public FeatureRequest(long timestamp, String key, LDContext context, int version, int variation, LDValue value,
+ LDValue defaultVal, EvaluationReason reason, String prereqOf, boolean trackEvents,
+ Long debugEventsUntilDate, boolean debug, long samplingRatio, boolean excludeFromSummaries) {
+ this(timestamp, key, context, version, variation, value, defaultVal, reason, prereqOf, trackEvents,
+ debugEventsUntilDate, debug, samplingRatio, excludeFromSummaries, false);
}
/**
@@ -311,6 +340,23 @@ public boolean isExcludeFromSummaries() {
return excludeFromSummaries;
}
+ /**
+ * True if an override affected this evaluation. The override can be direct or transitive. It is
+ * direct when the evaluated flag came from the SDK's override store. It is transitive when a
+ * prerequisite flag at any depth, or a segment read during the evaluation, came from that store.
+ *
+ * The event processor keys on this value alone. It produces no individual feature event and no
+ * debug event for such an evaluation, and it counts the evaluation in a separate summary counter
+ * that carries the override-affected marker.
+ *
+ * Flag overrides are currently experimental and subject to change.
+ *
+ * @return true if an override affected the evaluation
+ */
+ public boolean isOverrideAffected() {
+ return overrideAffected;
+ }
+
@Override
public long getSamplingRatio() {
return samplingRatio;
@@ -324,7 +370,7 @@ public long getSamplingRatio() {
public FeatureRequest toDebugEvent() {
return new FeatureRequest(getCreationDate(), getKey(), getContext(), getVersion(),
getVariation(), getValue(), getDefaultVal(), getReason(), getPrereqOf(),
- false, null, true, samplingRatio, excludeFromSummaries);
+ false, null, true, samplingRatio, excludeFromSummaries, overrideAffected);
}
}
diff --git a/lib/shared/internal/src/main/java/com/launchdarkly/sdk/internal/events/EventOutputFormatter.java b/lib/shared/internal/src/main/java/com/launchdarkly/sdk/internal/events/EventOutputFormatter.java
index 6815812d..71f98ea3 100644
--- a/lib/shared/internal/src/main/java/com/launchdarkly/sdk/internal/events/EventOutputFormatter.java
+++ b/lib/shared/internal/src/main/java/com/launchdarkly/sdk/internal/events/EventOutputFormatter.java
@@ -280,28 +280,11 @@ private void writeSummaryEvent(EventSummarizer.EventSummary summary, JsonWriter
jw.name("counters");
jw.beginArray();
- for (int i = 0; i < flagInfo.versionsAndVariations.size(); i++) {
- int version = flagInfo.versionsAndVariations.keyAt(i);
- SimpleIntKeyedMap variations = flagInfo.versionsAndVariations.valueAt(i);
- for (int j = 0; j < variations.size(); j++) {
- int variation = variations.keyAt(j);
- CounterValue counter = variations.valueAt(j);
-
- jw.beginObject();
-
- if (variation >= 0) {
- jw.name("variation").value(variation);
- }
- if (version >= 0) {
- jw.name("version").value(version);
- } else {
- jw.name("unknown").value(true);
- }
- writeLDValue("value", counter.flagValue, jw);
- jw.name("count").value(counter.count);
-
- jw.endObject();
- }
+ writeCounters(jw, flagInfo.versionsAndVariations, false);
+ SimpleIntKeyedMap> overrideAffectedCounters =
+ flagInfo.overrideAffectedVersionsAndVariationsIfAny();
+ if (overrideAffectedCounters != null) {
+ writeCounters(jw, overrideAffectedCounters, true);
}
jw.endArray(); // end of "counters" array
@@ -312,6 +295,37 @@ private void writeSummaryEvent(EventSummarizer.EventSummary summary, JsonWriter
jw.endObject(); // end of summary event object
}
+ private void writeCounters(JsonWriter jw, SimpleIntKeyedMap> versionsAndVariations,
+ boolean overrideAffected) throws IOException {
+ for (int i = 0; i < versionsAndVariations.size(); i++) {
+ int version = versionsAndVariations.keyAt(i);
+ SimpleIntKeyedMap variations = versionsAndVariations.valueAt(i);
+ for (int j = 0; j < variations.size(); j++) {
+ int variation = variations.keyAt(j);
+ CounterValue counter = variations.valueAt(j);
+
+ jw.beginObject();
+
+ if (variation >= 0) {
+ jw.name("variation").value(variation);
+ }
+ if (version >= 0) {
+ jw.name("version").value(version);
+ } else {
+ jw.name("unknown").value(true);
+ }
+ // The override-affected marker is present only when true, like the unknown marker.
+ if (overrideAffected) {
+ jw.name("overrideAffected").value(true);
+ }
+ writeLDValue("value", counter.flagValue, jw);
+ jw.name("count").value(counter.count);
+
+ jw.endObject();
+ }
+ }
+ }
+
private void writeKindAndCreationDate(JsonWriter jw, String kind, long creationDate) throws IOException {
jw.name("kind").value(kind);
jw.name("creationDate").value(creationDate);
diff --git a/lib/shared/internal/src/main/java/com/launchdarkly/sdk/internal/events/EventSummarizer.java b/lib/shared/internal/src/main/java/com/launchdarkly/sdk/internal/events/EventSummarizer.java
index e4a87daf..c1d795e2 100644
--- a/lib/shared/internal/src/main/java/com/launchdarkly/sdk/internal/events/EventSummarizer.java
+++ b/lib/shared/internal/src/main/java/com/launchdarkly/sdk/internal/events/EventSummarizer.java
@@ -47,7 +47,33 @@ void summarizeEvent(
LDValue defaultValue,
LDContext context
) {
- eventsState.incrementCounter(flagKey, flagVersion, variation, value, defaultValue, context);
+ summarizeEvent(timestamp, flagKey, flagVersion, variation, value, defaultValue, context, false);
+ }
+
+ /**
+ * Adds information about an evaluation to our counters. An override-affected evaluation is
+ * counted in a separate counter from other evaluations of the same flag, version, and variation.
+ *
+ * @param timestamp the millisecond timestamp
+ * @param flagKey the flag key
+ * @param flagVersion the flag version, or -1 if the flag is unknown
+ * @param variation the result variation, or -1 if none
+ * @param value the result value
+ * @param defaultValue the application default value
+ * @param context the evaluation context
+ * @param overrideAffected true if an override affected the evaluation
+ */
+ void summarizeEvent(
+ long timestamp,
+ String flagKey,
+ int flagVersion,
+ int variation,
+ LDValue value,
+ LDValue defaultValue,
+ LDContext context,
+ boolean overrideAffected
+ ) {
+ eventsState.incrementCounter(flagKey, flagVersion, variation, value, defaultValue, context, overrideAffected);
eventsState.noteTimestamp(timestamp);
}
@@ -120,7 +146,8 @@ void incrementCounter(
int variation,
LDValue flagValue,
LDValue defaultVal,
- LDContext context
+ LDContext context,
+ boolean overrideAffected
) {
FlagInfo flagInfo = counters.get(flagKey);
if (flagInfo == null) {
@@ -131,10 +158,12 @@ void incrementCounter(
flagInfo.contextKinds.add(context.getIndividualContext(i).getKind().toString());
}
- SimpleIntKeyedMap variations = flagInfo.versionsAndVariations.get(flagVersion);
+ SimpleIntKeyedMap> versionsAndVariations =
+ overrideAffected ? flagInfo.overrideAffectedVersionsAndVariations() : flagInfo.versionsAndVariations;
+ SimpleIntKeyedMap variations = versionsAndVariations.get(flagVersion);
if (variations == null) {
variations = new SimpleIntKeyedMap<>();
- flagInfo.versionsAndVariations.put(flagVersion, variations);
+ versionsAndVariations.put(flagVersion, variations);
}
CounterValue value = variations.get(variation);
@@ -175,21 +204,55 @@ public int hashCode() {
static final class FlagInfo {
final LDValue defaultVal;
+ // Counters for evaluations that no override affected, keyed by version and then by variation.
final SimpleIntKeyedMap> versionsAndVariations;
+ // Counters for override-affected evaluations, with the same shape. Most flags never have any,
+ // so the map is created on first use.
+ private SimpleIntKeyedMap> overrideAffectedVersionsAndVariations;
final Set contextKinds;
FlagInfo(LDValue defaultVal, SimpleIntKeyedMap> versionsAndVariations,
Set contextKinds) {
+ this(defaultVal, versionsAndVariations, null, contextKinds);
+ }
+
+ FlagInfo(LDValue defaultVal, SimpleIntKeyedMap> versionsAndVariations,
+ SimpleIntKeyedMap> overrideAffectedVersionsAndVariations,
+ Set contextKinds) {
this.defaultVal = defaultVal;
this.versionsAndVariations = versionsAndVariations;
+ this.overrideAffectedVersionsAndVariations = overrideAffectedVersionsAndVariations;
this.contextKinds = contextKinds;
}
+
+ /**
+ * Returns the counters for override-affected evaluations, creating the map on first use.
+ *
+ * @return the counters, never null
+ */
+ SimpleIntKeyedMap> overrideAffectedVersionsAndVariations() {
+ if (overrideAffectedVersionsAndVariations == null) {
+ overrideAffectedVersionsAndVariations = new SimpleIntKeyedMap<>();
+ }
+ return overrideAffectedVersionsAndVariations;
+ }
+
+ /**
+ * Returns the counters for override-affected evaluations, or null if there are none.
+ *
+ * @return the counters or null
+ */
+ SimpleIntKeyedMap> overrideAffectedVersionsAndVariationsIfAny() {
+ return overrideAffectedVersionsAndVariations == null || overrideAffectedVersionsAndVariations.size() == 0
+ ? null : overrideAffectedVersionsAndVariations;
+ }
@Override
public boolean equals(Object other) { // used only in tests
if (other instanceof FlagInfo) {
FlagInfo o = (FlagInfo)other;
return o.defaultVal.equals(this.defaultVal) && o.versionsAndVariations.equals(this.versionsAndVariations) &&
+ Objects.equals(o.overrideAffectedVersionsAndVariationsIfAny(), this.overrideAffectedVersionsAndVariationsIfAny()) &&
o.contextKinds.equals(this.contextKinds);
}
return false;
@@ -202,8 +265,10 @@ public int hashCode() { // used only in tests
@Override
public String toString() { // used only in tests
- return "(default=" + defaultVal + ", counters=" + versionsAndVariations + ", contextKinds=" +
- String.join(",", contextKinds) + ")";
+ return "(default=" + defaultVal + ", counters=" + versionsAndVariations +
+ (overrideAffectedVersionsAndVariationsIfAny() == null ? "" :
+ ", overrideAffectedCounters=" + overrideAffectedVersionsAndVariations) +
+ ", contextKinds=" + String.join(",", contextKinds) + ")";
}
}
diff --git a/lib/shared/internal/src/main/java/com/launchdarkly/sdk/internal/events/EventSummarizerInterface.java b/lib/shared/internal/src/main/java/com/launchdarkly/sdk/internal/events/EventSummarizerInterface.java
index 87a352f0..e138a89d 100644
--- a/lib/shared/internal/src/main/java/com/launchdarkly/sdk/internal/events/EventSummarizerInterface.java
+++ b/lib/shared/internal/src/main/java/com/launchdarkly/sdk/internal/events/EventSummarizerInterface.java
@@ -15,7 +15,7 @@
*/
public interface EventSummarizerInterface {
/**
- * Adds information about an evaluation to the summary.
+ * Adds information about an evaluation that no override affected to the summary.
*
* @param timestamp the millisecond timestamp
* @param flagKey the flag key
@@ -25,7 +25,7 @@ public interface EventSummarizerInterface {
* @param defaultValue the application default value
* @param context the evaluation context
*/
- void summarizeEvent(
+ default void summarizeEvent(
long timestamp,
String flagKey,
int flagVersion,
@@ -33,6 +33,34 @@ void summarizeEvent(
LDValue value,
LDValue defaultValue,
LDContext context
+ ) {
+ summarizeEvent(timestamp, flagKey, flagVersion, variation, value, defaultValue, context, false);
+ }
+
+ /**
+ * Adds information about an evaluation to the summary.
+ *
+ * Override-affected evaluations are counted separately from other evaluations of the same flag,
+ * version, and variation, so the marker takes part in the counter key.
+ *
+ * @param timestamp the millisecond timestamp
+ * @param flagKey the flag key
+ * @param flagVersion the flag version, or -1 if the flag is unknown
+ * @param variation the result variation, or -1 if none
+ * @param value the result value
+ * @param defaultValue the application default value
+ * @param context the evaluation context
+ * @param overrideAffected true if an override affected the evaluation
+ */
+ void summarizeEvent(
+ long timestamp,
+ String flagKey,
+ int flagVersion,
+ int variation,
+ LDValue value,
+ LDValue defaultValue,
+ LDContext context,
+ boolean overrideAffected
);
/**
diff --git a/lib/shared/internal/src/main/java/com/launchdarkly/sdk/internal/events/PerContextEventSummarizer.java b/lib/shared/internal/src/main/java/com/launchdarkly/sdk/internal/events/PerContextEventSummarizer.java
index d621378c..3158d642 100644
--- a/lib/shared/internal/src/main/java/com/launchdarkly/sdk/internal/events/PerContextEventSummarizer.java
+++ b/lib/shared/internal/src/main/java/com/launchdarkly/sdk/internal/events/PerContextEventSummarizer.java
@@ -36,6 +36,7 @@ public PerContextEventSummarizer() {
* @param value the result value
* @param defaultValue the application default value
* @param context the evaluation context
+ * @param overrideAffected true if an override affected the evaluation
*/
@Override
public void summarizeEvent(
@@ -45,13 +46,15 @@ public void summarizeEvent(
int variation,
LDValue value,
LDValue defaultValue,
- LDContext context
+ LDContext context,
+ boolean overrideAffected
) {
// Get or create summarizer for this context
EventSummarizer summarizer = summarizersByContext.computeIfAbsent(context, EventSummarizer::new);
// Delegate to the per-context summarizer
- summarizer.summarizeEvent(timestamp, flagKey, flagVersion, variation, value, defaultValue, context);
+ summarizer.summarizeEvent(timestamp, flagKey, flagVersion, variation, value, defaultValue, context,
+ overrideAffected);
}
/**
diff --git a/lib/shared/internal/src/test/java/com/launchdarkly/sdk/internal/events/BaseEventTest.java b/lib/shared/internal/src/test/java/com/launchdarkly/sdk/internal/events/BaseEventTest.java
index 776185cc..eb29e812 100644
--- a/lib/shared/internal/src/test/java/com/launchdarkly/sdk/internal/events/BaseEventTest.java
+++ b/lib/shared/internal/src/test/java/com/launchdarkly/sdk/internal/events/BaseEventTest.java
@@ -292,7 +292,19 @@ public static Matcher isSummaryEventCounter(int flagVersion, Inte
jsonProperty("variation", variation),
jsonProperty("version", (double)flagVersion),
jsonProperty("value", jsonFromValue(value)),
- jsonProperty("count", (double)count)
+ jsonProperty("count", (double)count),
+ jsonProperty("overrideAffected", jsonUndefined())
+ );
+ }
+
+ public static Matcher isOverrideAffectedSummaryEventCounter(int flagVersion, Integer variation,
+ LDValue value, int count) {
+ return allOf(
+ jsonProperty("variation", variation),
+ jsonProperty("version", (double)flagVersion),
+ jsonProperty("value", jsonFromValue(value)),
+ jsonProperty("count", (double)count),
+ jsonProperty("overrideAffected", true)
);
}
@@ -477,6 +489,7 @@ public static final class FeatureRequestEventBuilder {
private Long debugEventsUntilDate = null;
private long samplingRatio = 1;
private boolean excludeFromSummaries = false;
+ private boolean overrideAffected = false;
public FeatureRequestEventBuilder(LDContext context, String flagKey) {
this.context = context;
@@ -486,7 +499,12 @@ public FeatureRequestEventBuilder(LDContext context, String flagKey) {
public Event.FeatureRequest build() {
return new Event.FeatureRequest(timestamp, flagKey, context, flagVersion, variation, value,
defaultValue, reason, prereqOf, trackEvents, debugEventsUntilDate, false, samplingRatio,
- excludeFromSummaries);
+ excludeFromSummaries, overrideAffected);
+ }
+
+ public FeatureRequestEventBuilder overrideAffected(boolean overrideAffected) {
+ this.overrideAffected = overrideAffected;
+ return this;
}
public FeatureRequestEventBuilder flagVersion(int flagVersion) {
diff --git a/lib/shared/internal/src/test/java/com/launchdarkly/sdk/internal/events/DefaultEventProcessorOutputTest.java b/lib/shared/internal/src/test/java/com/launchdarkly/sdk/internal/events/DefaultEventProcessorOutputTest.java
index 78c4a19e..d5aaee58 100644
--- a/lib/shared/internal/src/test/java/com/launchdarkly/sdk/internal/events/DefaultEventProcessorOutputTest.java
+++ b/lib/shared/internal/src/test/java/com/launchdarkly/sdk/internal/events/DefaultEventProcessorOutputTest.java
@@ -351,6 +351,86 @@ public void identifyEventMakesIndexEventUnnecessary() throws Exception {
}
+ @SuppressWarnings("unchecked")
+ @Test
+ public void overrideAffectedEvaluationProducesNoFeatureEventAndNoDebugEvent() throws Exception {
+ // The flag requests both an individual feature event and a debug event. An override affected
+ // the evaluation, so neither is produced. The evaluation still produces an index event and a
+ // summary counter, and the counter carries the override-affected marker.
+ MockEventSender es = new MockEventSender();
+ long futureTime = System.currentTimeMillis() + 1000000;
+ Event.FeatureRequest fe = featureEvent(user, FLAG_KEY).flagVersion(FLAG_VERSION).variation(1)
+ .value(LDValue.of("value")).defaultValue(LDValue.of("default"))
+ .trackEvents(true).debugEventsUntilDate(futureTime).overrideAffected(true).build();
+
+ EventContextDeduplicator contextDeduplicator = contextDeduplicatorThatAlwaysSaysKeysAreNew();
+
+ try (DefaultEventProcessor ep = makeEventProcessor(baseConfig(es).contextDeduplicator(contextDeduplicator))) {
+ ep.sendEvent(fe);
+ }
+
+ assertThat(es.getEventsFromLastRequest(), contains(
+ isIndexEvent(fe, userJson),
+ allOf(
+ isSummaryEvent(),
+ hasSummaryFlag(FLAG_KEY, LDValue.of("default"),
+ contains(isOverrideAffectedSummaryEventCounter(FLAG_VERSION, 1, LDValue.of("value"), 1)))
+ )
+ ));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void overrideAffectedPrerequisiteEvaluationProducesNoFeatureEvent() throws Exception {
+ MockEventSender es = new MockEventSender();
+ Event.FeatureRequest fe = featureEvent(user, FLAG_KEY).prereqOf("parent")
+ .trackEvents(true).overrideAffected(true).build();
+
+ try (DefaultEventProcessor ep = makeEventProcessor(baseConfig(es))) {
+ ep.sendEvent(fe);
+ }
+
+ assertThat(es.getEventsFromLastRequest(), contains(
+ isSummaryEvent()
+ ));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void overrideAffectedAndUnaffectedEvaluationsOfSameFlagGetSeparateCounters() throws Exception {
+ MockEventSender es = new MockEventSender();
+ LDValue value = LDValue.of("value"), defaultValue = LDValue.of("default");
+ Event.FeatureRequest plain = featureEvent(user, FLAG_KEY).flagVersion(FLAG_VERSION).variation(1)
+ .value(value).defaultValue(defaultValue).build();
+ Event.FeatureRequest marked = featureEvent(user, FLAG_KEY).flagVersion(FLAG_VERSION).variation(1)
+ .value(value).defaultValue(defaultValue).overrideAffected(true).build();
+
+ try (DefaultEventProcessor ep = makeEventProcessor(baseConfig(es))) {
+ ep.sendEvent(plain);
+ ep.sendEvent(marked);
+ ep.sendEvent(marked);
+ }
+
+ assertThat(es.getEventsFromLastRequest(), contains(
+ allOf(
+ isSummaryEvent(),
+ hasSummaryFlag(FLAG_KEY, defaultValue,
+ Matchers.containsInAnyOrder(
+ isSummaryEventCounter(FLAG_VERSION, 1, value, 1),
+ isOverrideAffectedSummaryEventCounter(FLAG_VERSION, 1, value, 2)
+ ))
+ )
+ ));
+ }
+
+ @Test
+ public void debugEventKeepsOverrideAffectedMarker() {
+ Event.FeatureRequest fe = featureEvent(user, FLAG_KEY).overrideAffected(true).build();
+ Assert.assertTrue(fe.toDebugEvent().isOverrideAffected());
+ Assert.assertTrue(fe.toDebugEvent().isDebug());
+ Assert.assertFalse(featureEvent(user, FLAG_KEY).build().isOverrideAffected());
+ }
+
@SuppressWarnings("unchecked")
@Test
public void nonTrackedEventsAreSummarized() throws Exception {
diff --git a/lib/shared/internal/src/test/java/com/launchdarkly/sdk/internal/events/EventOutputTest.java b/lib/shared/internal/src/test/java/com/launchdarkly/sdk/internal/events/EventOutputTest.java
index 27ed2d15..e3582203 100644
--- a/lib/shared/internal/src/test/java/com/launchdarkly/sdk/internal/events/EventOutputTest.java
+++ b/lib/shared/internal/src/test/java/com/launchdarkly/sdk/internal/events/EventOutputTest.java
@@ -461,6 +461,34 @@ public void summaryEventIsSerialized() throws Exception {
));
}
+ @Test
+ public void summaryEventMarksOverrideAffectedCounters() throws Exception {
+ LDValue value = LDValue.of("value"), defaultValue = LDValue.of("default");
+ LDContext context = LDContext.create("key1");
+
+ EventSummarizer es = new EventSummarizer();
+ es.summarizeEvent(1000, "flag", 11, 1, value, defaultValue, context, false);
+ es.summarizeEvent(1001, "flag", 11, 1, value, defaultValue, context, true);
+ es.summarizeEvent(1002, "flag", 11, 1, value, defaultValue, context, true);
+ es.summarizeEvent(1003, "unknown", -1, -1, defaultValue, defaultValue, context, true);
+ EventSummary summary = es.getSummaryAndReset();
+
+ EventOutputFormatter f = new EventOutputFormatter(defaultEventsConfig());
+ StringWriter w = new StringWriter();
+ f.writeOutputEvents(new Event[0], Collections.singletonList(summary), w);
+ LDValue featuresJson = parseValue(w.toString()).get(0).get("features");
+
+ // The marked and unmarked evaluations of the same flag, version, and variation are separate
+ // counters. The marker appears only on the marked counter.
+ assertThat(featuresJson.get("flag").get("counters").values(), containsInAnyOrder(
+ parseValue("{\"value\":\"value\",\"variation\":1,\"version\":11,\"count\":1}"),
+ parseValue("{\"value\":\"value\",\"variation\":1,\"version\":11,\"overrideAffected\":true,\"count\":2}")
+ ));
+ assertThat(featuresJson.get("unknown").get("counters").values(), contains(
+ parseValue("{\"unknown\":true,\"overrideAffected\":true,\"value\":\"default\",\"count\":1}")
+ ));
+ }
+
@Test
public void migrationOpEventIsSerialized() throws IOException {
LDContext context = LDContext.builder("user-key").name("me").build();
diff --git a/lib/shared/internal/src/test/java/com/launchdarkly/sdk/internal/events/EventSummarizerTest.java b/lib/shared/internal/src/test/java/com/launchdarkly/sdk/internal/events/EventSummarizerTest.java
index 8426512f..d636e615 100644
--- a/lib/shared/internal/src/test/java/com/launchdarkly/sdk/internal/events/EventSummarizerTest.java
+++ b/lib/shared/internal/src/test/java/com/launchdarkly/sdk/internal/events/EventSummarizerTest.java
@@ -95,6 +95,56 @@ public void summarizeEventIncrementsCounters() {
.build()));
}
+ @Test
+ public void overrideAffectedEvaluationsAreCountedSeparately() {
+ EventSummarizer es = new EventSummarizer();
+ String flagKey = "key1";
+ int flagVersion = 11;
+ LDValue value1 = LDValue.of("value1"), default1 = LDValue.of("default1");
+ long timestamp = 1000;
+
+ // Same flag, version, variation, and value. Only the marker differs.
+ es.summarizeEvent(timestamp, flagKey, flagVersion, 1, value1, default1, context, false);
+ es.summarizeEvent(timestamp, flagKey, flagVersion, 1, value1, default1, context, true);
+ es.summarizeEvent(timestamp, flagKey, flagVersion, 1, value1, default1, context, true);
+
+ EventSummarizer.EventSummary data = es.getSummaryAndReset();
+
+ assertThat(data.counters, equalTo(ImmutableMap.builder()
+ .put(flagKey, new FlagInfo(default1,
+ new SimpleIntKeyedMap>()
+ .put(flagVersion, new SimpleIntKeyedMap()
+ .put(1, new CounterValue(1, value1))
+ ),
+ new SimpleIntKeyedMap>()
+ .put(flagVersion, new SimpleIntKeyedMap()
+ .put(1, new CounterValue(2, value1))
+ ),
+ ImmutableSet.of("user")))
+ .build()));
+ }
+
+ @Test
+ public void flagWithoutOverrideAffectedEvaluationsHasNoOverrideAffectedCounters() {
+ EventSummarizer es = new EventSummarizer();
+ es.summarizeEvent(1000, "key1", 11, 1, LDValue.of("v"), LDValue.of("d"), context);
+ EventSummarizer.EventSummary data = es.getSummaryAndReset();
+ assertNull(data.counters.get("key1").overrideAffectedVersionsAndVariationsIfAny());
+ }
+
+ @Test
+ public void flagInfoEqualityIncludesOverrideAffectedCounters() {
+ SimpleIntKeyedMap> plain = new SimpleIntKeyedMap>()
+ .put(1, new SimpleIntKeyedMap().put(0, new CounterValue(1, LDValue.of("a"))));
+ SimpleIntKeyedMap> marked = new SimpleIntKeyedMap>()
+ .put(1, new SimpleIntKeyedMap().put(0, new CounterValue(1, LDValue.of("a"))));
+ FlagInfo withMarked = new FlagInfo(LDValue.of("d"), plain, marked, ImmutableSet.of("user"));
+ FlagInfo withoutMarked = new FlagInfo(LDValue.of("d"), plain, ImmutableSet.of("user"));
+ assertNotEquals(withMarked, withoutMarked);
+ assertEquals(withMarked, new FlagInfo(LDValue.of("d"), plain, marked, ImmutableSet.of("user")));
+ assertThat(withMarked.toString(), org.hamcrest.Matchers.containsString("overrideAffectedCounters"));
+ }
+
// The following implementations are used only in debug/test code, but may as well test them
@Test