Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@ public void SummarizeEvent(
int? variation,
in LdValue value,
in LdValue defaultValue,
in Context context
in Context context,
bool overrideAffected
) =>
_summarizer.SummarizeEvent(timestamp, flagKey, flagVersion, variation, value, defaultValue, context);
_summarizer.SummarizeEvent(timestamp, flagKey, flagVersion, variation, value, defaultValue, context,
overrideAffected);

public IReadOnlyList<EventSummary> GetSummariesAndReset()
{
Expand Down
1 change: 1 addition & 0 deletions pkgs/shared/internal/src/Events/EventOutput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ public void WriteSummaryEvent(EventSummary summary, Utf8JsonWriter w)
JsonConverterHelpers.WriteLdValue(w, "value", counter.Value.FlagValue);
JsonConverterHelpers.WriteIntIfNotNull(w, "version", counter.Key.Version);
JsonConverterHelpers.WriteBooleanIfTrue(w, "unknown", !counter.Key.Version.HasValue);
JsonConverterHelpers.WriteBooleanIfTrue(w, "overrideAffected", counter.Key.OverrideAffected);
w.WriteNumber("count", counter.Value.Count);

w.WriteEndObject();
Expand Down
8 changes: 5 additions & 3 deletions pkgs/shared/internal/src/Events/EventProcessorInternal.cs
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ internal void AddEvent(object e)

internal void AddToSummary(EvaluationEvent ee) =>
_summarizer.SummarizeEvent(ee.Timestamp, ee.FlagKey, ee.FlagVersion, ee.Variation, ee.Value, ee.Default,
ee.Context);
ee.Context, ee.OverrideAffected);

internal FlushPayload GetPayload() =>
new FlushPayload { Events = _events.ToArray(), Summaries = _summarizer.GetSummariesAndReset() };
Expand Down Expand Up @@ -312,8 +312,10 @@ private void ProcessEvent(object e, EventBuffer buffer)
timestamp = ee.Timestamp;
context = ee.Context;
var samplingRatio = ee.SamplingRatio ?? 1;
willAddFullEvent = ee.TrackEvents && Sampler.Sample(samplingRatio);
if (ShouldDebugEvent(ee) && Sampler.Sample(samplingRatio))
// An override-affected evaluation appears only in the summary counters. It produces
// no individual feature event and no debug event, whatever the flag requests.
willAddFullEvent = ee.TrackEvents && !ee.OverrideAffected && Sampler.Sample(samplingRatio);
if (!ee.OverrideAffected && ShouldDebugEvent(ee) && Sampler.Sample(samplingRatio))
{
debugEvent = new DebugEvent { FromEvent = ee };
}
Expand Down
25 changes: 18 additions & 7 deletions pkgs/shared/internal/src/Events/EventSummarizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ public void SummarizeEvent(
int? variation,
in LdValue value,
in LdValue defaultValue,
in Context context
in Context context,
bool overrideAffected
)
{
_eventsState.IncrementCounter(flagKey, variation, flagVersion, value, defaultValue, context);
_eventsState.IncrementCounter(flagKey, variation, flagVersion, value, defaultValue, context, overrideAffected);
_eventsState.NoteTimestamp(timestamp);
}

Expand Down Expand Up @@ -71,7 +72,13 @@ public EventSummary(Context context)
Context = context;
}

public void IncrementCounter(string key, int? variation, int? version, LdValue flagValue, LdValue defaultVal, in Context context)
public void IncrementCounter(string key, int? variation, int? version, LdValue flagValue, LdValue defaultVal, in Context context) =>
IncrementCounter(key, variation, version, flagValue, defaultVal, context, false);

// The override-affected marker is part of the counter key. Override-affected and other
// evaluations of the same flag key, variation, and version accumulate into separate counters.
public void IncrementCounter(string key, int? variation, int? version, LdValue flagValue, LdValue defaultVal,
in Context context, bool overrideAffected)
{
if (!Flags.TryGetValue(key, out var flagSummary))
{
Expand All @@ -92,7 +99,7 @@ public void IncrementCounter(string key, int? variation, int? version, LdValue f
contextKinds.Add(context.Kind.Value);
}

EventsCounterKey counterKey = new EventsCounterKey(version, variation);
EventsCounterKey counterKey = new EventsCounterKey(version, variation, overrideAffected);
if (flagSummary.Counters.TryGetValue(counterKey, out EventsCounterValue value))
{
value.Increment();
Expand Down Expand Up @@ -135,26 +142,30 @@ internal sealed class EventsCounterKey
{
public readonly int? Version;
public readonly int? Variation;
public readonly bool OverrideAffected;

public EventsCounterKey(int? version, int? variation) : this(version, variation, false) { }

public EventsCounterKey(int? version, int? variation)
public EventsCounterKey(int? version, int? variation, bool overrideAffected)
{
Version = version;
Variation = variation;
OverrideAffected = overrideAffected;
}

// Required because we use this class as a dictionary key
public override bool Equals(object obj)
{
if (obj is EventsCounterKey o)
{
return Variation == o.Variation && Version == o.Version;
return Variation == o.Variation && Version == o.Version && OverrideAffected == o.OverrideAffected;
}
return false;
}

// Required because we use this class as a dictionary key
public override int GetHashCode() =>
(Variation ?? -1) * 17 + (Version ?? -1);
((Variation ?? -1) * 17 + (Version ?? -1)) * 2 + (OverrideAffected ? 1 : 0);
}

internal sealed class EventsCounterValue
Expand Down
4 changes: 4 additions & 0 deletions pkgs/shared/internal/src/Events/EventTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ public struct EvaluationEvent
// disabling events.
public long? SamplingRatio;
public bool ExcludeFromSummaries;
// True when a flag override affected the evaluation, directly or transitively. A marked
// evaluation produces no individual feature or debug event, whatever the flag requests, and
// is counted under a separate summary counter that carries the marker.
public bool OverrideAffected;
}

/// <summary>
Expand Down
6 changes: 4 additions & 2 deletions pkgs/shared/internal/src/Events/IEventSummarizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,17 @@ namespace LaunchDarkly.Sdk.Internal.Events
/// </remarks>
internal interface IEventSummarizer
{
// Adds information about an evaluation to the summary.
// Adds information about an evaluation to the summary. An override-affected evaluation is
// counted under a separate counter that carries the marker.
void SummarizeEvent(
UnixMillisecondTime timestamp,
string flagKey,
int? flagVersion,
int? variation,
in LdValue value,
in LdValue defaultValue,
in Context context
in Context context,
bool overrideAffected
);

// Returns the current summary data and resets the state to empty. The aggregated
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,17 @@ public void SummarizeEvent(
int? variation,
in LdValue value,
in LdValue defaultValue,
in Context context
in Context context,
bool overrideAffected
)
{
if (!_summarizersByContext.TryGetValue(context, out var summarizer))
{
summarizer = new EventSummarizer(context);
_summarizersByContext[context] = summarizer;
}
summarizer.SummarizeEvent(timestamp, flagKey, flagVersion, variation, value, defaultValue, context);
summarizer.SummarizeEvent(timestamp, flagKey, flagVersion, variation, value, defaultValue, context,
overrideAffected);
}

public IReadOnlyList<EventSummary> GetSummariesAndReset()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ public void EmptySummarizerReturnsNoSummaries()
public void AllContextsAreAggregatedIntoASingleContextlessSummary()
{
var summarizer = new AggregatedEventSummarizer();
summarizer.SummarizeEvent(Time, "flag", 1, 0, LdValue.Of("a"), LdValue.Null, ContextA);
summarizer.SummarizeEvent(Time, "flag", 1, 0, LdValue.Of("b"), LdValue.Null, ContextB);
summarizer.SummarizeEvent(Time, "flag", 1, 0, LdValue.Of("a"), LdValue.Null, ContextA, false);
summarizer.SummarizeEvent(Time, "flag", 1, 0, LdValue.Of("b"), LdValue.Null, ContextB, false);

var summaries = summarizer.GetSummariesAndReset();

Expand All @@ -35,7 +35,7 @@ public void AllContextsAreAggregatedIntoASingleContextlessSummary()
public void GetSummariesAndResetClearsState()
{
var summarizer = new AggregatedEventSummarizer();
summarizer.SummarizeEvent(Time, "flag", 1, 0, LdValue.Of("a"), LdValue.Null, ContextA);
summarizer.SummarizeEvent(Time, "flag", 1, 0, LdValue.Of("a"), LdValue.Null, ContextA, false);

Assert.Single(summarizer.GetSummariesAndReset());
Assert.Empty(summarizer.GetSummariesAndReset());
Expand Down
12 changes: 11 additions & 1 deletion pkgs/shared/internal/test/Events/EventOutputTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,9 @@ public void SummaryEventIsSerialized()
summary.IncrementCounter("third", null, null, LdValue.Of("default3"), LdValue.Of("default3"),
context2); // flag doesn't exist (no version)

summary.IncrementCounter("fourth", 1, 41, LdValue.Of("value4"), LdValue.Of("default4"), context1, true);
summary.IncrementCounter("fourth", 1, 41, LdValue.Of("value4"), LdValue.Of("default4"), context1, false);

summary.NoteTimestamp(UnixMillisecondTime.OfMillis(1000));
summary.NoteTimestamp(UnixMillisecondTime.OfMillis(1002));

Expand All @@ -217,7 +220,7 @@ public void SummaryEventIsSerialized()
Assert.Equal(LdValue.Null, outputEvent.Get("context"));

var featuresJson = outputEvent.Get("features");
Assert.Equal(3, featuresJson.Count);
Assert.Equal(4, featuresJson.Count);

var firstJson = featuresJson.Get("first");
Assert.Equal("default1", firstJson.Get("default").AsString);
Expand All @@ -242,6 +245,13 @@ public void SummaryEventIsSerialized()
thirdJson.Get("contextKinds")); // we evaluated this flag with only context2
TestUtil.AssertContainsInAnyOrder(thirdJson.Get("counters").AsList(LdValue.Convert.Json),
LdValue.Parse(@"{""unknown"":true,""value"":""default3"",""count"":1}"));

// An override-affected counter carries the marker. The counter for the same flag,
// variation, and version without the marker stays separate and omits the property.
var fourthJson = featuresJson.Get("fourth");
TestUtil.AssertContainsInAnyOrder(fourthJson.Get("counters").List,
LdValue.Parse(@"{""value"":""value4"",""variation"":1,""version"":41,""overrideAffected"":true,""count"":1}"),
LdValue.Parse(@"{""value"":""value4"",""variation"":1,""version"":41,""count"":1}"));
}

[Fact]
Expand Down
104 changes: 100 additions & 4 deletions pkgs/shared/internal/test/Events/EventProcessorTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ private void RecordEval(EventProcessor ep, TestFlagProperties f, TestEvalPropert
Reason = e.Reason,
PrereqOf = e.PrereqOf,
TrackEvents = f.TrackEvents,
DebugEventsUntilDate = f.DebugEventsUntilDate
DebugEventsUntilDate = f.DebugEventsUntilDate,
OverrideAffected = e.OverrideAffected
});
}

Expand Down Expand Up @@ -189,6 +190,95 @@ public void IndividualFeatureEventIsQueuedWithIndexEvent()
}
}

[Fact]
public void OverrideAffectedEvaluationProducesNoFeatureEvent()
{
var mockSender = MakeMockSender();
var captured = EventCapture.From(mockSender);

using (var ep = MakeProcessor(_config, mockSender))
{
var eval = BasicEval;
eval.OverrideAffected = true;
RecordEval(ep, BasicFlagWithTracking, eval);
FlushAndWait(ep, captured);

// The flag requests individual events, but the marking alone decides: only the index
// event and the summary appear.
Assert.Collection(captured.Events,
item => CheckIndexEvent(item, BasicEval.Timestamp, _contextJson),
item => CheckSummaryEventDetails(item, BasicEval.Timestamp, BasicEval.Timestamp,
MustHaveFlagSummary(BasicFlag.Key, LdValue.Null,
MustHaveFlagSummaryCounter(BasicEval.Value, BasicEval.Variation, BasicFlag.Version, 1, true))));
}
}

[Fact]
public void OverrideAffectedPrerequisiteRecordProducesNoFeatureEvent()
{
var mockSender = MakeMockSender();
var captured = EventCapture.From(mockSender);

using (var ep = MakeProcessor(_config, mockSender))
{
var eval = BasicEval;
eval.OverrideAffected = true;
eval.PrereqOf = "parent-flag";
RecordEval(ep, BasicFlagWithTracking, eval);
FlushAndWait(ep, captured);

Assert.Collection(captured.Events,
item => CheckIndexEvent(item, BasicEval.Timestamp, _contextJson),
item => CheckSummaryEvent(item));
}
}

[Fact]
public void OverrideAffectedEvaluationProducesNoDebugEvent()
{
var mockSender = MakeMockSender();
var captured = EventCapture.From(mockSender);

using (var ep = MakeProcessor(_config, mockSender))
{
var flag = BasicFlagWithTracking;
flag.DebugEventsUntilDate = UnixMillisecondTime.Now.PlusMillis(1000000);
var eval = BasicEval;
eval.OverrideAffected = true;
RecordEval(ep, flag, eval);
FlushAndWait(ep, captured);

Assert.Collection(captured.Events,
item => CheckIndexEvent(item, BasicEval.Timestamp, _contextJson),
item => CheckSummaryEvent(item));
}
}

[Fact]
public void OverrideAffectedEvaluationsAreCountedSeparatelyFromOthers()
{
var mockSender = MakeMockSender();
var captured = EventCapture.From(mockSender);

using (var ep = MakeProcessor(_config, mockSender))
{
var plainEval = BasicEval;
var markedEval = BasicEval;
markedEval.OverrideAffected = true;
RecordEval(ep, BasicFlag, markedEval);
RecordEval(ep, BasicFlag, markedEval);
RecordEval(ep, BasicFlag, plainEval);
FlushAndWait(ep, captured);

Assert.Collection(captured.Events,
item => CheckIndexEvent(item, BasicEval.Timestamp, _contextJson),
item => CheckSummaryEventDetails(item, BasicEval.Timestamp, BasicEval.Timestamp,
MustHaveFlagSummary(BasicFlag.Key, LdValue.Null,
MustHaveFlagSummaryCounter(BasicEval.Value, BasicEval.Variation, BasicFlag.Version, 2, true),
MustHaveFlagSummaryCounter(BasicEval.Value, BasicEval.Variation, BasicFlag.Version, 1, false))));
}
}

[Fact]
public void ItDoesNotQueueAFeatureEventWithSamplingRatioOfZero()
{
Expand Down Expand Up @@ -965,7 +1055,12 @@ private Action<LdValue> MustHaveFlagSummary(string flagKey, LdValue defaultVal,
};
}

private Action<string, LdValue> MustHaveFlagSummaryCounter(LdValue value, int? variation, int? version, int count)
private Action<string, LdValue> MustHaveFlagSummaryCounter(LdValue value, int? variation, int? version, int count) =>
MustHaveFlagSummaryCounter(value, variation, version, count, false);

// An override-affected counter carries "overrideAffected": true. Any other counter omits the property.
private Action<string, LdValue> MustHaveFlagSummaryCounter(LdValue value, int? variation, int? version, int count,
bool overrideAffected)
{
return (flagKey, items) =>
{
Expand All @@ -974,11 +1069,12 @@ private Action<string, LdValue> MustHaveFlagSummaryCounter(LdValue value, int? v
return o.Get("value").Equals(value)
&& o.Get("version").Equals(version.HasValue ? LdValue.Of(version.Value) : LdValue.Null)
&& o.Get("variation").Equals(variation.HasValue ? LdValue.Of(variation.Value) : LdValue.Null)
&& o.Get("count").Equals(LdValue.Of(count));
&& o.Get("count").Equals(LdValue.Of(count))
&& o.Get("overrideAffected").Equals(overrideAffected ? LdValue.Of(true) : LdValue.Null);
}))
{
Assert.True(false, "could not find counter for (" + value + ", " + version + ", " + variation + ", " + count
+ ") in: " + items.ToString() + " for flag " + flagKey);
+ ", overrideAffected=" + overrideAffected + ") in: " + items.ToString() + " for flag " + flagKey);
}
};
}
Expand Down
Loading
Loading