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