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