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
40 changes: 24 additions & 16 deletions lib/ldclient-rb/events.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ def record_eval_event(
debug_until = nil,
prereq_of = nil,
sampling_ratio = nil,
exclude_from_summaries = false
exclude_from_summaries = false,
override_affected = false
)
end

Expand Down Expand Up @@ -176,10 +177,11 @@ def record_eval_event(
debug_until = nil,
prereq_of = nil,
sampling_ratio = nil,
exclude_from_summaries = false
exclude_from_summaries = false,
override_affected = false
)
post_to_inbox(LaunchDarkly::Impl::EvalEvent.new(timestamp, context, key, version, variation, value, reason,
default, track_events, debug_until, prereq_of, sampling_ratio, exclude_from_summaries))
default, track_events, debug_until, prereq_of, sampling_ratio, exclude_from_summaries, override_affected))
end

def record_identify_event(context)
Expand Down Expand Up @@ -328,8 +330,10 @@ def dispatch_event(event, outbox)
will_add_full_event = false
debug_event = nil
if event.is_a?(LaunchDarkly::Impl::EvalEvent)
will_add_full_event = event.track_events
if should_debug_event(event)
# 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.
will_add_full_event = event.track_events && !event.override_affected
if !event.override_affected && should_debug_event(event)
debug_event = LaunchDarkly::Impl::DebugEvent.new(event)
end
else
Expand Down Expand Up @@ -630,18 +634,22 @@ def make_output_events(events, summary)
summary.counters.each do |flagKey, flagInfo|
counters = []
flagInfo.versions.each do |version, variations|
variations.each do |variation, counter|
c = {
value: counter.value,
count: counter.count,
}
c[:variation] = variation unless variation.nil?
if version.nil?
c[:unknown] = true
else
c[:version] = version
variations.each do |variation, counters_by_marker|
counters_by_marker.each do |override_affected, counter|
c = {
value: counter.value,
count: counter.count,
}
c[:variation] = variation unless variation.nil?
if version.nil?
c[:unknown] = true
else
c[:version] = version
end
# The marker is present only for override-affected counters, like the unknown marker.
c[:overrideAffected] = true if override_affected
counters.push(c)
end
counters.push(c)
end
end
flags[flagKey] = { default: flagInfo.default, counters: counters, contextKinds: flagInfo.context_kinds.to_a }
Expand Down
15 changes: 13 additions & 2 deletions lib/ldclient-rb/impl/event_summarizer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ module LaunchDarkly
module Impl
EventSummary = Struct.new(:start_date, :end_date, :counters)

# The `versions` member is a hash of hashes: flag version, then variation index, then the
# override-affected marker (a boolean), to an EventSummaryFlagVariationCounter. The marker
# takes part in the key, so override-affected and other evaluations of the same flag,
# variation, and version accumulate into separate counters.
EventSummaryFlagInfo = Struct.new(:default, :versions, :context_kinds)

EventSummaryFlagVariationCounter = Struct.new(:value, :count)
Expand Down Expand Up @@ -39,9 +43,16 @@ def summarize_event(event)

counters_for_flag.context_kinds.merge(event.context.kinds)

variation_counter = counters_for_flag_version[event.variation]
counters_for_variation = counters_for_flag_version[event.variation]
if counters_for_variation.nil?
counters_for_variation = Hash.new
counters_for_flag_version[event.variation] = counters_for_variation
end

override_affected = event.override_affected
variation_counter = counters_for_variation[override_affected]
if variation_counter.nil?
counters_for_flag_version[event.variation] = EventSummaryFlagVariationCounter.new(event.value, 1)
counters_for_variation[override_affected] = EventSummaryFlagVariationCounter.new(event.value, 1)
else
variation_counter.count = variation_counter.count + 1
end
Expand Down
12 changes: 11 additions & 1 deletion lib/ldclient-rb/impl/event_types.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ def initialize(timestamp, context, sampling_ratio = nil, exclude_from_summaries

class EvalEvent < Event
def initialize(timestamp, context, key, version = nil, variation = nil, value = nil, reason = nil, default = nil,
track_events = false, debug_until = nil, prereq_of = nil, sampling_ratio = nil, exclude_from_summaries = false)
track_events = false, debug_until = nil, prereq_of = nil, sampling_ratio = nil, exclude_from_summaries = false,
override_affected = false)
super(timestamp, context, sampling_ratio, exclude_from_summaries)
@key = key
@version = version
Expand All @@ -38,6 +39,7 @@ def initialize(timestamp, context, key, version = nil, variation = nil, value =
@track_events = track_events if track_events
@debug_until = debug_until if debug_until
@prereq_of = prereq_of if prereq_of
@override_affected = true if override_affected
end

attr_reader :key
Expand All @@ -49,6 +51,14 @@ def initialize(timestamp, context, key, version = nil, variation = nil, value =
attr_reader :track_events
attr_reader :debug_until
attr_reader :prereq_of

# True if an override affected the evaluation, directly or through a prerequisite or segment.
# Such an evaluation appears only in the summary counters, under a counter that carries the
# override-affected marker. It produces no individual feature event and no debug event.
# @return [Boolean]
def override_affected
@override_affected ? true : false
end
end

class MigrationOpEvent < Event
Expand Down
45 changes: 32 additions & 13 deletions lib/ldclient-rb/ldclient.rb
Original file line number Diff line number Diff line change
Expand Up @@ -697,16 +697,27 @@ def all_flags_state(context, options={})
end

requires_experiment_data = experiment?(f, detail.reason)
track_events = f[:trackEvents] || requires_experiment_data
track_reason = requires_experiment_data
debug_events_until_date = f[:debugEventsUntilDate]
if eval_result&.override_affected
# A consumer of this state sends individual events according to these fields. An
# override-affected evaluation produces no individual events, so the state turns them off
# for this flag. The flag, its value, and its reason stay.
track_events = false
track_reason = false
debug_events_until_date = nil
end
flag_state = {
key: f[:key],
value: detail.value,
variation: detail.variation_index,
reason: detail.reason,
prerequisites: eval_state.prerequisites,
version: f[:version],
trackEvents: f[:trackEvents] || requires_experiment_data,
trackReason: requires_experiment_data,
debugEventsUntilDate: f[:debugEventsUntilDate],
trackEvents: track_events,
trackReason: track_reason,
debugEventsUntilDate: debug_events_until_date,
}

state.add_flag(flag_state, with_reasons, details_only_if_tracked)
Expand Down Expand Up @@ -835,25 +846,29 @@ def flag_tracker
begin
(res, _) = @evaluator.evaluate(feature, context)
unless res.prereq_evals.nil?
# Each prerequisite record carries the marking of the prerequisite's own evaluation, not
# the marking of the evaluation that requested it.
res.prereq_evals.each do |prereq_eval|
record_prereq_flag_eval(prereq_eval.prereq_flag, prereq_eval.prereq_of_flag, context, prereq_eval.detail, with_reasons)
record_prereq_flag_eval(prereq_eval.prereq_flag, prereq_eval.prereq_of_flag, context, prereq_eval.detail,
with_reasons, prereq_eval.override_affected)
end
end
detail = res.detail
if detail.default_value?
detail = EvaluationDetail.new(default, nil, detail.reason)
end
record_flag_eval(feature, context, detail, default, with_reasons)
record_flag_eval(feature, context, detail, default, with_reasons, res.override_affected)
[detail, feature, nil]
rescue => exn
Impl::Util.log_exception(@config.logger, "Error evaluating feature flag \"#{key}\"", exn)
detail = Evaluator.error_result(EvaluationReason::ERROR_EXCEPTION, default)
record_flag_eval_error(feature, context, default, detail.reason, with_reasons)
# The flag definition was read before the failure, so its own marker decides the marking.
record_flag_eval_error(feature, context, default, detail.reason, with_reasons, feature.override?)
[detail, feature, exn.to_s]
end
end

private def record_flag_eval(flag, context, detail, default, with_reasons)
private def record_flag_eval(flag, context, detail, default, with_reasons, override_affected)
add_experiment_data = experiment?(flag, detail.reason)
@event_processor.record_eval_event(
context,
Expand All @@ -867,11 +882,12 @@ def flag_tracker
flag[:debugEventsUntilDate],
nil,
flag[:samplingRatio],
!!flag[:excludeFromSummaries]
!!flag[:excludeFromSummaries],
override_affected
)
end

private def record_prereq_flag_eval(prereq_flag, prereq_of_flag, context, detail, with_reasons)
private def record_prereq_flag_eval(prereq_flag, prereq_of_flag, context, detail, with_reasons, override_affected)
add_experiment_data = experiment?(prereq_flag, detail.reason)
@event_processor.record_eval_event(
context,
Expand All @@ -885,13 +901,15 @@ def flag_tracker
prereq_flag[:debugEventsUntilDate],
prereq_of_flag[:key],
prereq_flag[:samplingRatio],
!!prereq_flag[:excludeFromSummaries]
!!prereq_flag[:excludeFromSummaries],
override_affected
)
end

private def record_flag_eval_error(flag, context, default, reason, with_reasons)
private def record_flag_eval_error(flag, context, default, reason, with_reasons, override_affected)
@event_processor.record_eval_event(context, flag[:key], flag[:version], nil, default, with_reasons ? reason : nil, default,
flag[:trackEvents], flag[:debugEventsUntilDate], nil, flag[:samplingRatio], !!flag[:excludeFromSummaries])
flag[:trackEvents], flag[:debugEventsUntilDate], nil, flag[:samplingRatio], !!flag[:excludeFromSummaries],
override_affected)
end

#
Expand All @@ -902,8 +920,9 @@ def flag_tracker
# @param with_reasons [Boolean]
#
private def record_unknown_flag_eval(flag_key, context, default, reason, with_reasons)
# Nothing was read from the override store, so the evaluation is not marked.
@event_processor.record_eval_event(context, flag_key, nil, nil, default, with_reasons ? reason : nil, default,
false, nil, nil, 1, false)
false, nil, nil, 1, false, false)
end

private def experiment?(flag, reason)
Expand Down
77 changes: 77 additions & 0 deletions spec/events_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,83 @@ module LaunchDarkly
end
end

describe "override-affected evaluations" do
let(:future_time) { (Time.now.to_f * 1000).to_i + 1000000 }

it "does not produce a feature event even when the flag tracks events" do
with_processor_and_sender(default_config, starting_timestamp) do |ep, sender|
ep.record_eval_event(context, 'flagkey', 11, 1, 'value', nil, 'default', true, nil, nil, nil, false, true)

output = flush_and_get_events(ep, sender)
expect(output).to contain_exactly(
eq(index_event(default_config, context)),
include(:kind => "summary")
)
end
end

it "does not produce a debug event even when the flag is in debug mode" do
with_processor_and_sender(default_config, starting_timestamp) do |ep, sender|
ep.record_eval_event(context, 'flagkey', 11, 1, 'value', nil, 'default', true, future_time, nil, nil, false, true)

output = flush_and_get_events(ep, sender)
expect(output).to contain_exactly(
eq(index_event(default_config, context)),
include(:kind => "summary")
)
end
end

it "is counted in a summary counter that carries the override-affected marker" do
with_processor_and_sender(default_config, starting_timestamp) do |ep, sender|
ep.record_eval_event(context, 'flagkey', 11, 1, 'value', nil, 'default', true, nil, nil, nil, false, true)
ep.record_eval_event(context, 'flagkey', 11, 1, 'value', nil, 'default', true, nil, nil, nil, false, true)

output = flush_and_get_events(ep, sender)
summary = output.detect { |e| e[:kind] == "summary" }
expect(summary[:features][:flagkey][:counters]).to contain_exactly(
{ version: 11, variation: 1, value: "value", count: 2, overrideAffected: true }
)
end
end

it "is counted separately from other evaluations of the same flag, version, and variation" do
with_processor_and_sender(default_config, starting_timestamp) do |ep, sender|
ep.record_eval_event(context, 'flagkey', 11, 1, 'value', nil, 'default', false, nil, nil, nil, false, true)
ep.record_eval_event(context, 'flagkey', 11, 1, 'value', nil, 'default', false)

output = flush_and_get_events(ep, sender)
summary = output.detect { |e| e[:kind] == "summary" }
expect(summary[:features][:flagkey][:counters]).to contain_exactly(
{ version: 11, variation: 1, value: "value", count: 1, overrideAffected: true },
{ version: 11, variation: 1, value: "value", count: 1 }
)
end
end

it "does not add the marker to counters for other evaluations" do
with_processor_and_sender(default_config, starting_timestamp) do |ep, sender|
ep.record_eval_event(context, 'flagkey', 11, 1, 'value', nil, 'default', false)

output = flush_and_get_events(ep, sender)
summary = output.detect { |e| e[:kind] == "summary" }
expect(summary[:features][:flagkey][:counters]).to eq([{ version: 11, variation: 1, value: "value", count: 1 }])
end
end

it "keeps a marked prerequisite record out of the individual events" do
with_processor_and_sender(default_config, starting_timestamp) do |ep, sender|
ep.record_eval_event(context, 'prereq', 11, 1, 'value', nil, nil, true, nil, 'top', nil, false, true)

output = flush_and_get_events(ep, sender)
expect(output).to contain_exactly(
eq(index_event(default_config, context)),
include(:kind => "summary")
)
end
end
end

it "queues custom event with context" do
with_processor_and_sender(default_config, starting_timestamp) do |ep, sender|
ep.record_custom_event(context, 'eventkey', { thing: 'stuff' }, 1.5)
Expand Down
32 changes: 28 additions & 4 deletions spec/impl/event_summarizer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,24 +55,48 @@ module Impl
'key1' => EventSummaryFlagInfo.new(
'default1', {
11 => {
1 => EventSummaryFlagVariationCounter.new('value1', 2),
2 => EventSummaryFlagVariationCounter.new('value2', 1),
1 => { false => EventSummaryFlagVariationCounter.new('value1', 2) },
2 => { false => EventSummaryFlagVariationCounter.new('value2', 1) },
},
},
Set.new(["user"])
),
'key2' => EventSummaryFlagInfo.new(
'default2', {
22 => {
1 => EventSummaryFlagVariationCounter.new('value99', 1),
1 => { false => EventSummaryFlagVariationCounter.new('value99', 1) },
},
},
Set.new(["user"])
),
'badkey' => EventSummaryFlagInfo.new(
'default3', {
nil => {
nil => EventSummaryFlagVariationCounter.new('default3', 1),
nil => { false => EventSummaryFlagVariationCounter.new('default3', 1) },
},
},
Set.new(["user"])
),
}
expect(data.counters).to eq expected_counters
end

it "counts override-affected evaluations separately from other evaluations of the same flag, version, and variation" do
es = subject.new
plain = make_eval_event(0, context, 'key1', 11, 1, 'value1', nil, 'default1')
marked = LaunchDarkly::Impl::EvalEvent.new(0, context, 'key1', 11, 1, 'value1', nil, 'default1',
false, nil, nil, nil, false, true)
[plain, marked, marked].each { |e| es.summarize_event(e) }
data = es.snapshot

expected_counters = {
'key1' => EventSummaryFlagInfo.new(
'default1', {
11 => {
1 => {
false => EventSummaryFlagVariationCounter.new('value1', 1),
true => EventSummaryFlagVariationCounter.new('value1', 2),
},
},
},
Set.new(["user"])
Expand Down
Loading
Loading