Skip to content

Commit e1dd774

Browse files
committed
feat: Suppress individual events and split summary counters for override-affected evaluations
The OVERRIDE specification requires an override-affected evaluation to produce no individual feature event and no debug event, whatever the flag requests, while still being counted in summary events under a separate counter that carries the overrideAffected marker. A prerequisite record carries the prerequisite's own marking. EventProcessor gains an overload of recordEvaluationEvent with the marking. It is a default method that delegates to the existing method, so custom implementations keep compiling. The SDK's implementations override it and the evaluator's event recorder passes the evaluation result's marking for top-level, prerequisite, and requested-type mismatch records. Event generation keys on that value alone. The server SDK now depends on launchdarkly-java-sdk-internal 1.13.0, which carries the marker through Event.FeatureRequest, the summarizer, the output formatter, and the event processor. CI for this change cannot pass until that version is released. The specification's vector runner now also asserts the per-evaluation summary marking. Flag overrides are currently experimental and subject to change.
1 parent 51b497a commit e1dd774

9 files changed

Lines changed: 391 additions & 13 deletions

File tree

‎lib/sdk/server/build.gradle‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ ext.versions = [
7171
"guava": "32.0.1-jre",
7272
"jackson": "2.11.2",
7373
"launchdarklyJavaSdkCommon": "2.6.0",
74-
"launchdarklyJavaSdkInternal": "1.11.1",
74+
"launchdarklyJavaSdkInternal": "1.13.0",
7575
"launchdarklyLogging": "1.1.0",
7676
"okhttp": "4.12.0", // specify this for the SDK build instead of relying on the transitive dependency from okhttp-eventsource
7777
"okhttpEventsource": "5.0.0",

‎lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/DefaultEventProcessorWrapper.java‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,15 @@ public void recordEvaluationEvent(LDContext context, String flagKey, int flagVer
3434
LDValue value, EvaluationReason reason, LDValue defaultValue, String prerequisiteOfFlagKey,
3535
boolean requireFullEvent, Long debugEventsUntilDate, boolean excludeFromSummaries,
3636
Long samplingRatio) {
37+
recordEvaluationEvent(context, flagKey, flagVersion, variation, value, reason, defaultValue, prerequisiteOfFlagKey,
38+
requireFullEvent, debugEventsUntilDate, excludeFromSummaries, samplingRatio, false);
39+
}
40+
41+
@Override
42+
public void recordEvaluationEvent(LDContext context, String flagKey, int flagVersion, int variation,
43+
LDValue value, EvaluationReason reason, LDValue defaultValue, String prerequisiteOfFlagKey,
44+
boolean requireFullEvent, Long debugEventsUntilDate, boolean excludeFromSummaries,
45+
Long samplingRatio, boolean overrideAffected) {
3746
eventProcessor.sendEvent(new Event.FeatureRequest(
3847
System.currentTimeMillis(),
3948
flagKey,
@@ -48,7 +57,8 @@ public void recordEvaluationEvent(LDContext context, String flagKey, int flagVer
4857
debugEventsUntilDate,
4958
false,
5059
samplingRatio != null ? samplingRatio : 1,
51-
excludeFromSummaries
60+
excludeFromSummaries,
61+
overrideAffected
5262
));
5363
}
5464

‎lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/EvaluationRecorder.java‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ default void recordEvaluation(DataModel.FeatureFlag flag, LDContext context, Eva
1616
default void recordPrerequisiteEvaluation(DataModel.FeatureFlag flag, DataModel.FeatureFlag prereqOfFlag, LDContext context, EvalResult result) {
1717
// default is no op
1818
}
19-
default void recordEvaluationError(DataModel.FeatureFlag flag, LDContext context, LDValue defaultValue, EvaluationReason.ErrorKind errorKind) {
19+
default void recordEvaluationError(DataModel.FeatureFlag flag, LDContext context, LDValue defaultValue,
20+
EvaluationReason.ErrorKind errorKind, boolean overrideAffected) {
2021
// default is no op
2122
}
2223
default void recordEvaluationUnknownFlagError(String flagKey, LDContext context, LDValue defaultValue, EvaluationReason.ErrorKind errorKind) {

‎lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/InputValidatingEvaluator.java‎

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,8 @@ EvalResultAndFlag evaluate(String flagKey, LDContext context, LDValue defaultVal
144144
!value.isNull() &&
145145
value.getType() != requireType) {
146146
logger.error("Feature flag \"{}\"; evaluation expected result as {}, but got {}", flagKey, defaultValue.getType(), value.getType());
147-
recorder.recordEvaluationError(featureFlag, context, defaultValue, ErrorKind.WRONG_TYPE);
147+
recorder.recordEvaluationError(featureFlag, context, defaultValue, ErrorKind.WRONG_TYPE,
148+
result.isOverrideAffected());
148149
// The type mismatch replaces the reason. The evaluation read the same definitions, so the
149150
// new reason keeps the override-affected marking.
150151
return new EvalResultAndFlag(
@@ -163,7 +164,8 @@ EvalResultAndFlag evaluate(String flagKey, LDContext context, LDValue defaultVal
163164
if (featureFlag == null) {
164165
recorder.recordEvaluationUnknownFlagError(flagKey, context, defaultValue, ErrorKind.EXCEPTION);
165166
} else {
166-
recorder.recordEvaluationError(featureFlag, context, defaultValue, ErrorKind.EXCEPTION);
167+
// The evaluation did not complete, so nothing is known about which definitions it read.
168+
recorder.recordEvaluationError(featureFlag, context, defaultValue, ErrorKind.EXCEPTION, false);
167169
}
168170
return new EvalResultAndFlag(EvalResult.of(defaultValue, NO_VARIATION, EvaluationReason.exception(e)), null);
169171
}
@@ -255,6 +257,8 @@ private static DataModel.Segment getSegment(ReadOnlyStore store, String key) {
255257
*/
256258
private static EvaluationRecorder makeEvaluationRecorder(EventProcessor processor, boolean withReasons) {
257259
return new EvaluationRecorder() {
260+
// The marking passed to the event processor is the evaluation result's own record of the
261+
// definitions it read. Event generation does not read the evaluation reason.
258262
@Override
259263
public void recordEvaluation(FeatureFlag flag, LDContext context, EvalResult result, LDValue defaultValue) {
260264
processor.recordEvaluationEvent(
@@ -269,10 +273,13 @@ public void recordEvaluation(FeatureFlag flag, LDContext context, EvalResult res
269273
flag.isTrackEvents() || result.isForceReasonTracking(),
270274
flag.getDebugEventsUntilDate(),
271275
flag.isExcludeFromSummaries(),
272-
flag.getSamplingRatio()
276+
flag.getSamplingRatio(),
277+
result.isOverrideAffected()
273278
);
274279
}
275280

281+
// A prerequisite record carries the prerequisite's own marking, which its own subtree of
282+
// reads set. It is not the marking of the evaluation that requested it.
276283
@Override
277284
public void recordPrerequisiteEvaluation(FeatureFlag flag, FeatureFlag prereqOfFlag, LDContext context, EvalResult result) {
278285
processor.recordEvaluationEvent(
@@ -287,25 +294,28 @@ public void recordPrerequisiteEvaluation(FeatureFlag flag, FeatureFlag prereqOfF
287294
flag.isTrackEvents() || result.isForceReasonTracking(),
288295
flag.getDebugEventsUntilDate(),
289296
flag.isExcludeFromSummaries(),
290-
flag.getSamplingRatio()
297+
flag.getSamplingRatio(),
298+
result.isOverrideAffected()
291299
);
292300
}
293301

294302
@Override
295-
public void recordEvaluationError(FeatureFlag flag, LDContext context, LDValue defaultValue, ErrorKind errorKind) {
303+
public void recordEvaluationError(FeatureFlag flag, LDContext context, LDValue defaultValue, ErrorKind errorKind,
304+
boolean overrideAffected) {
296305
processor.recordEvaluationEvent(
297306
context,
298307
flag.getKey(),
299308
flag.getVersion(),
300309
NO_VARIATION,
301310
defaultValue,
302-
withReasons ? EvaluationReason.error(errorKind) : null,
311+
withReasons ? EvaluationReason.error(errorKind).withOverrideAffected(overrideAffected) : null,
303312
defaultValue,
304313
null,
305314
flag.isTrackEvents(),
306315
flag.getDebugEventsUntilDate(),
307316
flag.isExcludeFromSummaries(),
308-
flag.getSamplingRatio()
317+
flag.getSamplingRatio(),
318+
overrideAffected
309319
);
310320
}
311321

‎lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/NoOpEventProcessor.java‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ public void recordEvaluationEvent(LDContext context, String flagKey, int flagVer
2020
// no-op
2121
}
2222

23+
@Override
24+
public void recordEvaluationEvent(LDContext context, String flagKey, int flagVersion, int variation, LDValue value,
25+
EvaluationReason reason, LDValue defaultValue, String prerequisiteOfFlagKey,
26+
boolean requireFullEvent, Long debugEventsUntilDate, boolean excludeFromSummaries,
27+
Long samplingRatio, boolean overrideAffected) {
28+
// no-op
29+
}
30+
2331
@Override
2432
public void recordIdentifyEvent(LDContext context) {
2533
// no-op

‎lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/subsystems/EventProcessor.java‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,54 @@ void recordEvaluationEvent(
5757
boolean excludeFromSummaries,
5858
Long samplingRatio
5959
);
60+
61+
/**
62+
* Records the action of evaluating a feature flag, with the override-affected marking.
63+
* <p>
64+
* An override-affected evaluation is one that read at least one definition from the SDK's
65+
* override store: the flag itself, a prerequisite at any depth, or a segment consulted during
66+
* matching. Such an evaluation produces no individual feature event and no debug event, whatever
67+
* the flag's configuration requests, and it is counted in a separate summary counter that
68+
* carries the marker. The SDK keys this behavior on the marking alone, not on the reason.
69+
* <p>
70+
* The default implementation ignores the marking and calls the method without it, so an
71+
* existing custom implementation keeps working. Flag overrides are currently experimental and
72+
* subject to change.
73+
*
74+
* @param context the evaluation context
75+
* @param flagKey key of the feature flag that was evaluated
76+
* @param flagVersion the version of the flag, or {@link #NO_VERSION} if the flag was not found
77+
* @param variation the result variation index, or {@link EvaluationDetail#NO_VARIATION} if evaluation failed
78+
* @param value the result value
79+
* @param reason the evaluation reason, or null if the reason was not requested
80+
* @param defaultValue the default value parameter for the evaluation
81+
* @param prerequisiteOfFlagKey the key of the flag that this flag was evaluated as a prerequisite of,
82+
* or null if this flag was evaluated for itself
83+
* @param requireFullEvent true if full-fidelity analytics events should be sent for this flag
84+
* @param debugEventsUntilDate if non-null, debug events are to be generated until this millisecond time
85+
* @param excludeFromSummaries true if the event evaluation should not be included in summaries
86+
* @param samplingRatio ratio used to control event sampling
87+
* @param overrideAffected true if an override affected the evaluation
88+
* @since 7.18.0
89+
*/
90+
default void recordEvaluationEvent(
91+
LDContext context,
92+
String flagKey,
93+
int flagVersion,
94+
int variation,
95+
LDValue value,
96+
EvaluationReason reason,
97+
LDValue defaultValue,
98+
String prerequisiteOfFlagKey,
99+
boolean requireFullEvent,
100+
Long debugEventsUntilDate,
101+
boolean excludeFromSummaries,
102+
Long samplingRatio,
103+
boolean overrideAffected
104+
) {
105+
recordEvaluationEvent(context, flagKey, flagVersion, variation, value, reason, defaultValue,
106+
prerequisiteOfFlagKey, requireFullEvent, debugEventsUntilDate, excludeFromSummaries, samplingRatio);
107+
}
60108

61109
/**
62110
* Registers an evaluation context, as when the SDK's {@code identify} method is called.

0 commit comments

Comments
 (0)