From 0e7744f2770e644153c0b5f304161ab0a9682a77 Mon Sep 17 00:00:00 2001
From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com>
Date: Fri, 25 Sep 2026 15:05:12 -0700
Subject: [PATCH] feat: Count override-affected evaluations separately and send
no individual events for them
The OVERRIDE specification marks an evaluation as override-affected when
any definition it read came from the SDK's override store. Such an
evaluation appears in summary events only: it 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
an overrideAffected marker so that override-affected and ordinary
evaluations of the same flag, variation, and version are never collapsed
together.
Event.FeatureRequest carries the marking as a boolean that the SDK sets
from its evaluation result. The existing constructors keep working and
leave it false. DefaultEventProcessor keys individual event and debug
event emission on it. EventSummarizer keeps a second set of counters per
flag for marked evaluations, created on first use, and the output
formatter writes overrideAffected only on those counters, like the
unknown marker. EventSummarizerInterface gains the marker parameter and
keeps the previous signature as a default method.
Flag overrides are currently experimental and subject to change.
---
.../events/AggregatedEventSummarizer.java | 6 +-
.../events/DefaultEventProcessor.java | 9 ++-
.../sdk/internal/events/Event.java | 52 +++++++++++-
.../internal/events/EventOutputFormatter.java | 58 +++++++++-----
.../sdk/internal/events/EventSummarizer.java | 77 ++++++++++++++++--
.../events/EventSummarizerInterface.java | 32 +++++++-
.../events/PerContextEventSummarizer.java | 7 +-
.../sdk/internal/events/BaseEventTest.java | 22 ++++-
.../DefaultEventProcessorOutputTest.java | 80 +++++++++++++++++++
.../sdk/internal/events/EventOutputTest.java | 28 +++++++
.../internal/events/EventSummarizerTest.java | 50 ++++++++++++
11 files changed, 379 insertions(+), 42 deletions(-)
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