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 811af12c..59a8dde7 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 @@ -145,7 +145,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( @@ -161,14 +162,14 @@ EvalResultAndFlag evaluate(String flagKey, LDContext context, LDValue defaultVal logger.error("Encountered exception while evaluating feature flag \"{}\": {}", flagKey, LogValues.exceptionSummary(e)); logger.debug("{}", LogValues.exceptionTrace(e)); + // The flag's definition was read before the failure, so its own marker decides the marking of + // the error event and of the error result. + boolean overrideAffected = featureFlag != null && featureFlag.isOverride(); if (featureFlag == null) { recorder.recordEvaluationUnknownFlagError(flagKey, context, defaultValue, ErrorKind.EXCEPTION); } else { - recorder.recordEvaluationError(featureFlag, context, defaultValue, ErrorKind.EXCEPTION); + recorder.recordEvaluationError(featureFlag, context, defaultValue, ErrorKind.EXCEPTION, overrideAffected); } - // The flag's definition was read before the failure, so its own marker decides the marking of - // the error result. - boolean overrideAffected = featureFlag != null && featureFlag.isOverride(); return new EvalResultAndFlag( EvalResult.of(defaultValue, NO_VARIATION, EvaluationReason.exception(e)).withOverrideAffected(overrideAffected), null); @@ -285,6 +286,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( @@ -299,10 +302,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( @@ -317,25 +323,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/InputValidatingEvaluatorOverrideTest.java b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/InputValidatingEvaluatorOverrideTest.java
index 8ffdd167..b8442ee3 100644
--- a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/InputValidatingEvaluatorOverrideTest.java
+++ b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/InputValidatingEvaluatorOverrideTest.java
@@ -4,8 +4,10 @@
import com.launchdarkly.sdk.EvaluationReason.ErrorKind;
import com.launchdarkly.sdk.LDValue;
import com.launchdarkly.sdk.LDValueType;
+import com.launchdarkly.sdk.internal.events.Event;
import com.launchdarkly.sdk.json.JsonSerialization;
import com.launchdarkly.sdk.server.DataModel.FeatureFlag;
+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;
@@ -94,6 +96,21 @@ public void exceptionWhileEvaluatingOverrideFlagKeepsMarking() {
assertTrue(result.getResult().isOverrideAffected());
}
+ @Test
+ public void exceptionWhileEvaluatingOverrideFlagRecordsAMarkedErrorEvent() {
+ TestEventProcessor events = new TestEventProcessor();
+ FeatureFlag flag = flagWithPrerequisite("flag").markedAsOverride();
+ new InputValidatingEvaluator(storeFailingOn("prereq", flag), null, null, events, nullLogger)
+ .evalAndFlag("LDClient.stringVariation", "flag", BASE_USER, LDValue.of("default"), LDValueType.STRING,
+ EvaluationOptions.EVENTS_WITH_REASONS);
+
+ assertEquals(1, events.events.size());
+ Event.FeatureRequest record = (Event.FeatureRequest) events.events.get(0);
+ assertTrue(record.isOverrideAffected());
+ assertEquals(EvaluationReason.ErrorKind.EXCEPTION, record.getReason().getErrorKind());
+ assertTrue(record.getReason().isOverrideAffected());
+ }
+
@Test
public void exceptionWhileEvaluatingPlainFlagIsNotMarked() {
FeatureFlag flag = flagWithPrerequisite("flag");
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