diff --git a/lib/ldclient-rb.rb b/lib/ldclient-rb.rb index 9a215686..2bff8c8f 100644 --- a/lib/ldclient-rb.rb +++ b/lib/ldclient-rb.rb @@ -21,7 +21,6 @@ module LaunchDarkly require "ldclient-rb/user_filter" require "ldclient-rb/simple_lru_cache" require "ldclient-rb/non_blocking_thread_pool" -require "ldclient-rb/event_summarizer" require "ldclient-rb/events" require "ldclient-rb/requestor" require "ldclient-rb/file_data_source" diff --git a/lib/ldclient-rb/event_summarizer.rb b/lib/ldclient-rb/event_summarizer.rb deleted file mode 100644 index c48a400f..00000000 --- a/lib/ldclient-rb/event_summarizer.rb +++ /dev/null @@ -1,55 +0,0 @@ - -module LaunchDarkly - # @private - EventSummary = Struct.new(:start_date, :end_date, :counters) - - # Manages the state of summarizable information for the EventProcessor, including the - # event counters and user deduplication. Note that the methods of this class are - # deliberately not thread-safe; the EventProcessor is responsible for enforcing - # synchronization across both the summarizer and the event queue. - # - # @private - class EventSummarizer - def initialize - clear - end - - # Adds this event to our counters, if it is a type of event we need to count. - def summarize_event(event) - if event[:kind] == "feature" - counter_key = { - key: event[:key], - version: event[:version], - variation: event[:variation] - } - c = @counters[counter_key] - if c.nil? - @counters[counter_key] = { - value: event[:value], - default: event[:default], - count: 1 - } - else - c[:count] = c[:count] + 1 - end - time = event[:creationDate] - if !time.nil? - @start_date = time if @start_date == 0 || time < @start_date - @end_date = time if time > @end_date - end - end - end - - # Returns a snapshot of the current summarized event data, and resets this state. - def snapshot - ret = EventSummary.new(@start_date, @end_date, @counters) - ret - end - - def clear - @start_date = 0 - @end_date = 0 - @counters = {} - end - end -end diff --git a/lib/ldclient-rb/events.rb b/lib/ldclient-rb/events.rb index 7b77c4db..f2b3e9f9 100644 --- a/lib/ldclient-rb/events.rb +++ b/lib/ldclient-rb/events.rb @@ -1,5 +1,7 @@ require "ldclient-rb/impl/diagnostic_events" require "ldclient-rb/impl/event_sender" +require "ldclient-rb/impl/event_summarizer" +require "ldclient-rb/impl/event_types" require "ldclient-rb/impl/util" require "concurrent" @@ -26,16 +28,33 @@ # module LaunchDarkly - MAX_FLUSH_WORKERS = 5 - USER_ATTRS_TO_STRINGIFY_FOR_EVENTS = [ :key, :secondary, :ip, :country, :email, :firstName, :lastName, - :avatar, :name ] + module EventProcessorMethods + def record_eval_event( + user, + key, + version = nil, + variation = nil, + value = nil, + reason = nil, + default = nil, + track_events = false, + debug_until = nil, + prereq_of = nil + ) + end - private_constant :MAX_FLUSH_WORKERS - private_constant :USER_ATTRS_TO_STRINGIFY_FOR_EVENTS + def record_identify_event(user) + end - # @private - class NullEventProcessor - def add_event(event) + def record_custom_event( + user, + key, + data = nil, + metric_value = nil + ) + end + + def record_alias_event(user, previous_user) end def flush @@ -45,12 +64,16 @@ def stop end end + MAX_FLUSH_WORKERS = 5 + USER_ATTRS_TO_STRINGIFY_FOR_EVENTS = [ :key, :secondary, :ip, :country, :email, :firstName, :lastName, + :avatar, :name ] + + private_constant :MAX_FLUSH_WORKERS + private_constant :USER_ATTRS_TO_STRINGIFY_FOR_EVENTS + # @private - class EventMessage - def initialize(event) - @event = event - end - attr_reader :event + class NullEventProcessor + include EventProcessorMethods end # @private @@ -90,6 +113,8 @@ class StopMessage < SynchronousMessage # @private class EventProcessor + include EventProcessorMethods + def initialize(sdk_key, config, client = nil, diagnostic_accumulator = nil, test_properties = nil) raise ArgumentError, "sdk_key must not be nil" if sdk_key.nil? # see LDClient constructor comment on sdk_key @logger = config.logger @@ -116,16 +141,46 @@ def initialize(sdk_key, config, client = nil, diagnostic_accumulator = nil, test @stopped = Concurrent::AtomicBoolean.new(false) @inbox_full = Concurrent::AtomicBoolean.new(false) - event_sender = test_properties && test_properties.has_key?(:event_sender) ? - test_properties[:event_sender] : + event_sender = (test_properties || {})[:event_sender] || Impl::EventSender.new(sdk_key, config, client ? client : Util.new_http_client(config.events_uri, config)) + @timestamp_fn = (test_properties || {})[:timestamp_fn] || proc { Impl::Util.current_time_millis } + EventDispatcher.new(@inbox, sdk_key, config, diagnostic_accumulator, event_sender) end - def add_event(event) - event[:creationDate] = Impl::Util.current_time_millis - post_to_inbox(EventMessage.new(event)) + def record_eval_event( + user, + key, + version = nil, + variation = nil, + value = nil, + reason = nil, + default = nil, + track_events = false, + debug_until = nil, + prereq_of = nil + ) + post_to_inbox(LaunchDarkly::Impl::EvalEvent.new(timestamp, user, key, version, variation, value, reason, + default, track_events, debug_until, prereq_of)) + end + + def record_identify_event(user) + post_to_inbox(LaunchDarkly::Impl::IdentifyEvent.new(timestamp, user)) + end + + def record_custom_event(user, key, data = nil, metric_value = nil) + post_to_inbox(LaunchDarkly::Impl::CustomEvent.new(timestamp, user, key, data, metric_value)) + end + + def record_alias_event(user, previous_user) + post_to_inbox(LaunchDarkly::Impl::AliasEvent.new( + timestamp, + user.nil? ? nil : user[:key], + user_to_context_kind(user), + previous_user.nil? ? nil : previous_user[:key], + user_to_context_kind(previous_user) + )) end def flush @@ -155,9 +210,11 @@ def wait_until_inactive sync_msg.wait_for_completion end - private + private def timestamp + @timestamp_fn.call() + end - def post_to_inbox(message) + private def post_to_inbox(message) begin @inbox.push(message, non_block=true) rescue ThreadError @@ -170,6 +227,10 @@ def post_to_inbox(message) end end end + + private def user_to_context_kind(user) + (user.nil? || !user[:anonymous]) ? 'user' : 'anonymousUser' + end end # @private @@ -209,8 +270,6 @@ def main_loop(inbox, outbox, flush_workers, diagnostic_event_workers) begin message = inbox.pop case message - when EventMessage - dispatch_event(message.event, outbox) when FlushMessage trigger_flush(outbox, flush_workers) when FlushUsersMessage @@ -224,6 +283,8 @@ def main_loop(inbox, outbox, flush_workers, diagnostic_event_workers) do_shutdown(flush_workers, diagnostic_event_workers) running = false message.completed + else + dispatch_event(message, outbox) end rescue => e Util.log_exception(@config.logger, "Unexpected error in event processor", e) @@ -257,11 +318,10 @@ def dispatch_event(event, outbox) # the event (if tracked) and once for debugging. will_add_full_event = false debug_event = nil - if event[:kind] == "feature" - will_add_full_event = event[:trackEvents] + if event.is_a?(LaunchDarkly::Impl::EvalEvent) + will_add_full_event = event.track_events if should_debug_event(event) - debug_event = event.clone - debug_event[:debug] = true + debug_event = LaunchDarkly::Impl::DebugEvent.new(event) end else will_add_full_event = true @@ -270,12 +330,8 @@ def dispatch_event(event, outbox) # For each user we haven't seen before, we add an index event - unless this is already # an identify event for that user. if !(will_add_full_event && @config.inline_users_in_events) - if event.has_key?(:user) && !notice_user(event[:user]) && event[:kind] != "identify" - outbox.add_event({ - kind: "index", - creationDate: event[:creationDate], - user: event[:user] - }) + if !event.user.nil? && !notice_user(event.user) && !event.is_a?(LaunchDarkly::Impl::IdentifyEvent) + outbox.add_event(LaunchDarkly::Impl::IndexEvent.new(event.timestamp, event.user)) end end @@ -295,7 +351,7 @@ def notice_user(user) end def should_debug_event(event) - debug_until = event[:debugEventsUntilDate] + debug_until = event.debug_until if !debug_until.nil? last_past = @last_known_past_time.value debug_until > last_past && debug_until > Impl::Util.current_time_millis @@ -365,12 +421,11 @@ def initialize(capacity, logger) @capacity_exceeded = false @dropped_events = 0 @events = [] - @summarizer = EventSummarizer.new + @summarizer = LaunchDarkly::Impl::EventSummarizer.new end def add_event(event) if @events.length < @capacity - @logger.debug { "[LDClient] Enqueueing event: #{event.to_json}" } @events.push(event) @capacity_exceeded = false else @@ -404,6 +459,15 @@ def clear # @private class EventOutputFormatter + FEATURE_KIND = 'feature' + IDENTIFY_KIND = 'identify' + CUSTOM_KIND = 'custom' + ALIAS_KIND = 'alias' + INDEX_KIND = 'index' + DEBUG_KIND = 'debug' + SUMMARY_KIND = 'summary' + ANONYMOUS_USER_CONTEXT_KIND = 'anonymousUser' + def initialize(config) @inline_users = config.inline_users_in_events @user_filter = UserFilter.new(config) @@ -418,100 +482,130 @@ def make_output_events(events, summary) events_out end - private - - def process_user(event) - filtered = @user_filter.transform_user_props(event[:user]) - Util.stringify_attrs(filtered, USER_ATTRS_TO_STRINGIFY_FOR_EVENTS) - end - - def make_output_event(event) - case event[:kind] - when "feature" - is_debug = event[:debug] + private def make_output_event(event) + case event + + when LaunchDarkly::Impl::EvalEvent out = { - kind: is_debug ? "debug" : "feature", - creationDate: event[:creationDate], - key: event[:key], - value: event[:value] + kind: FEATURE_KIND, + creationDate: event.timestamp, + key: event.key, + value: event.value } - out[:default] = event[:default] if event.has_key?(:default) - out[:variation] = event[:variation] if event.has_key?(:variation) - out[:version] = event[:version] if event.has_key?(:version) - out[:prereqOf] = event[:prereqOf] if event.has_key?(:prereqOf) - out[:contextKind] = event[:contextKind] if event.has_key?(:contextKind) - if @inline_users || is_debug - out[:user] = process_user(event) - else - out[:userKey] = event[:user][:key] - end - out[:reason] = event[:reason] if !event[:reason].nil? + out[:default] = event.default if !event.default.nil? + out[:variation] = event.variation if !event.variation.nil? + out[:version] = event.version if !event.version.nil? + out[:prereqOf] = event.prereq_of if !event.prereq_of.nil? + set_opt_context_kind(out, event.user) + set_user_or_user_key(out, event.user) + out[:reason] = event.reason if !event.reason.nil? out - when "identify" + + when LaunchDarkly::Impl::IdentifyEvent { - kind: "identify", - creationDate: event[:creationDate], - key: event[:user][:key].to_s, - user: process_user(event) + kind: IDENTIFY_KIND, + creationDate: event.timestamp, + key: event.user[:key].to_s, + user: process_user(event.user) } - when "custom" + + when LaunchDarkly::Impl::CustomEvent out = { - kind: "custom", - creationDate: event[:creationDate], - key: event[:key] + kind: CUSTOM_KIND, + creationDate: event.timestamp, + key: event.key } - out[:data] = event[:data] if event.has_key?(:data) - if @inline_users - out[:user] = process_user(event) - else - out[:userKey] = event[:user][:key] - end - out[:metricValue] = event[:metricValue] if event.has_key?(:metricValue) - out[:contextKind] = event[:contextKind] if event.has_key?(:contextKind) + out[:data] = event.data if !event.data.nil? + set_user_or_user_key(out, event.user) + out[:metricValue] = event.metric_value if !event.metric_value.nil? + set_opt_context_kind(out, event.user) out - when "index" + + when LaunchDarkly::Impl::AliasEvent + { + kind: ALIAS_KIND, + creationDate: event.timestamp, + key: event.key, + contextKind: event.context_kind, + previousKey: event.previous_key, + previousContextKind: event.previous_context_kind + } + + when LaunchDarkly::Impl::IndexEvent { - kind: "index", - creationDate: event[:creationDate], - user: process_user(event) + kind: INDEX_KIND, + creationDate: event.timestamp, + user: process_user(event.user) } + + when LaunchDarkly::Impl::DebugEvent + original = event.eval_event + out = { + kind: DEBUG_KIND, + creationDate: original.timestamp, + key: original.key, + user: process_user(original.user), + value: original.value + } + out[:default] = original.default if !original.default.nil? + out[:variation] = original.variation if !original.variation.nil? + out[:version] = original.version if !original.version.nil? + out[:prereqOf] = original.prereq_of if !original.prereq_of.nil? + set_opt_context_kind(out, original.user) + out[:reason] = original.reason if !original.reason.nil? + out + else - event + nil end end # Transforms the summary data into the format used for event sending. - def make_summary_event(summary) + private def make_summary_event(summary) flags = {} - summary[:counters].each { |ckey, cval| - flag = flags[ckey[:key]] - if flag.nil? - flag = { - default: cval[:default], - counters: [] - } - flags[ckey[:key]] = flag - end - c = { - value: cval[:value], - count: cval[:count] - } - if !ckey[:variation].nil? - c[:variation] = ckey[:variation] - end - if ckey[:version].nil? - c[:unknown] = true - else - c[:version] = ckey[:version] + 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 if !variation.nil? + if version.nil? + c[:unknown] = true + else + c[:version] = version + end + counters.push(c) + end end - flag[:counters].push(c) - } + flags[flagKey] = { default: flagInfo.default, counters: counters } + end { - kind: "summary", + kind: SUMMARY_KIND, startDate: summary[:start_date], endDate: summary[:end_date], features: flags } end + + private def set_opt_context_kind(out, user) + out[:contextKind] = ANONYMOUS_USER_CONTEXT_KIND if !user.nil? && user[:anonymous] + end + + private def set_user_or_user_key(out, user) + if @inline_users + out[:user] = process_user(user) + else + key = user[:key] + out[:userKey] = key.is_a?(String) ? key : key.to_s + end + end + + private def process_user(user) + filtered = @user_filter.transform_user_props(user) + Util.stringify_attrs(filtered, USER_ATTRS_TO_STRINGIFY_FOR_EVENTS) + end end end diff --git a/lib/ldclient-rb/impl/evaluator.rb b/lib/ldclient-rb/impl/evaluator.rb index 9e10c8ef..ed94719e 100644 --- a/lib/ldclient-rb/impl/evaluator.rb +++ b/lib/ldclient-rb/impl/evaluator.rb @@ -4,6 +4,13 @@ module LaunchDarkly module Impl + # Used internally to record that we evaluated a prerequisite flag. + PrerequisiteEvalRecord = Struct.new( + :prereq_flag, # the prerequisite flag that we evaluated + :prereq_of_flag, # the flag that it was a prerequisite of + :detail # the EvaluationDetail representing the evaluation result + ) + # Encapsulates the feature flag evaluation logic. The Evaluator has no knowledge of the rest of the SDK environment; # if it needs to retrieve flags or segments that are referenced by a flag, it does so through a simple function that # is provided in the constructor. It also produces feature requests as appropriate for any referenced prerequisite @@ -22,7 +29,7 @@ def initialize(get_flag, get_segment, get_big_segments_membership, logger) @get_big_segments_membership = get_big_segments_membership @logger = logger end - + # Used internally to hold an evaluation result and additional state that may be accumulated during an # evaluation. It's simpler and a bit more efficient to represent these as mutable properties rather than # trying to use a pure functional approach, and since we're not exposing this object to any application code @@ -34,7 +41,7 @@ def initialize(get_flag, get_segment, get_big_segments_membership, logger) # evaluation. EvalResult = Struct.new( :detail, # the EvaluationDetail representing the evaluation result - :events, # an array of evaluation events generated by prerequisites, or nil + :prereq_evals, # an array of PrerequisiteEvalRecord instances, or nil :big_segments_status, :big_segments_membership ) @@ -50,17 +57,15 @@ def self.error_result(errorKind, value = nil) # # @param flag [Object] the flag # @param user [Object] the user properties - # @param event_factory [EventFactory] called to construct a feature request event when a prerequisite flag is - # evaluated; the caller is responsible for constructing the feature event for the top-level evaluation # @return [EvalResult] the evaluation result - def evaluate(flag, user, event_factory) + def evaluate(flag, user) result = EvalResult.new if user.nil? || user[:key].nil? result.detail = Evaluator.error_result(EvaluationReason::ERROR_USER_NOT_SPECIFIED) return result end - detail = eval_internal(flag, user, result, event_factory) + detail = eval_internal(flag, user, result) if !result.big_segments_status.nil? # If big_segments_status is non-nil at the end of the evaluation, it means a query was done at # some point and we will want to include the status in the evaluation reason. @@ -80,12 +85,12 @@ def self.make_big_segment_ref(segment) # method is visible for testing private - def eval_internal(flag, user, state, event_factory) + def eval_internal(flag, user, state) if !flag[:on] return get_off_value(flag, EvaluationReason::off) end - prereq_failure_reason = check_prerequisites(flag, user, state, event_factory) + prereq_failure_reason = check_prerequisites(flag, user, state) if !prereq_failure_reason.nil? return get_off_value(flag, prereq_failure_reason) end @@ -118,7 +123,7 @@ def eval_internal(flag, user, state, event_factory) return EvaluationDetail.new(nil, nil, EvaluationReason::fallthrough) end - def check_prerequisites(flag, user, state, event_factory) + def check_prerequisites(flag, user, state) (flag[:prerequisites] || []).each do |prerequisite| prereq_ok = true prereq_key = prerequisite[:key] @@ -129,15 +134,15 @@ def check_prerequisites(flag, user, state, event_factory) prereq_ok = false else begin - prereq_res = eval_internal(prereq_flag, user, state, event_factory) + prereq_res = eval_internal(prereq_flag, user, state) # Note that if the prerequisite flag is off, we don't consider it a match no matter what its # off variation was. But we still need to evaluate it in order to generate an event. if !prereq_flag[:on] || prereq_res.variation_index != prerequisite[:variation] prereq_ok = false end - event = event_factory.new_eval_event(prereq_flag, user, prereq_res, nil, flag) - state.events = [] if state.events.nil? - state.events.push(event) + prereq_eval = PrerequisiteEvalRecord.new(prereq_flag, flag, prereq_res) + state.prereq_evals = [] if state.prereq_evals.nil? + state.prereq_evals.push(prereq_eval) rescue => exn Util.log_exception(@logger, "Error evaluating prerequisite flag \"#{prereq_key}\" for flag \"#{flag[:key]}\"", exn) prereq_ok = false diff --git a/lib/ldclient-rb/impl/event_factory.rb b/lib/ldclient-rb/impl/event_factory.rb deleted file mode 100644 index 19b4e474..00000000 --- a/lib/ldclient-rb/impl/event_factory.rb +++ /dev/null @@ -1,123 +0,0 @@ - -module LaunchDarkly - module Impl - # Event constructors are centralized here to avoid mistakes and repetitive logic. - # The LDClient owns two instances of EventFactory: one that always embeds evaluation reasons - # in the events (for when variation_detail is called) and one that doesn't. - # - # Note that these methods do not set the "creationDate" property, because in the Ruby client, - # that is done by EventProcessor.add_event(). - class EventFactory - def initialize(with_reasons) - @with_reasons = with_reasons - end - - def new_eval_event(flag, user, detail, default_value, prereq_of_flag = nil) - add_experiment_data = self.class.is_experiment(flag, detail.reason) - e = { - kind: 'feature', - key: flag[:key], - user: user, - variation: detail.variation_index, - value: detail.value, - default: default_value, - version: flag[:version] - } - # the following properties are handled separately so we don't waste bandwidth on unused keys - e[:trackEvents] = true if add_experiment_data || flag[:trackEvents] - e[:debugEventsUntilDate] = flag[:debugEventsUntilDate] if flag[:debugEventsUntilDate] - e[:prereqOf] = prereq_of_flag[:key] if !prereq_of_flag.nil? - e[:reason] = detail.reason if add_experiment_data || @with_reasons - e[:contextKind] = context_to_context_kind(user) if !user.nil? && user[:anonymous] - e - end - - def new_default_event(flag, user, default_value, reason) - e = { - kind: 'feature', - key: flag[:key], - user: user, - value: default_value, - default: default_value, - version: flag[:version] - } - e[:trackEvents] = true if flag[:trackEvents] - e[:debugEventsUntilDate] = flag[:debugEventsUntilDate] if flag[:debugEventsUntilDate] - e[:reason] = reason if @with_reasons - e[:contextKind] = context_to_context_kind(user) if !user.nil? && user[:anonymous] - e - end - - def new_unknown_flag_event(key, user, default_value, reason) - e = { - kind: 'feature', - key: key, - user: user, - value: default_value, - default: default_value - } - e[:reason] = reason if @with_reasons - e[:contextKind] = context_to_context_kind(user) if !user.nil? && user[:anonymous] - e - end - - def new_identify_event(user) - { - kind: 'identify', - key: user[:key], - user: user - } - end - - def new_alias_event(current_context, previous_context) - { - kind: 'alias', - key: current_context[:key], - contextKind: context_to_context_kind(current_context), - previousKey: previous_context[:key], - previousContextKind: context_to_context_kind(previous_context) - } - end - - def new_custom_event(event_name, user, data, metric_value) - e = { - kind: 'custom', - key: event_name, - user: user - } - e[:data] = data if !data.nil? - e[:metricValue] = metric_value if !metric_value.nil? - e[:contextKind] = context_to_context_kind(user) if !user.nil? && user[:anonymous] - e - end - - def self.is_experiment(flag, reason) - return false if !reason - - if reason.in_experiment - return true - end - - case reason[:kind] - when 'RULE_MATCH' - index = reason[:ruleIndex] - if !index.nil? - rules = flag[:rules] || [] - return index >= 0 && index < rules.length && rules[index][:trackEvents] - end - when 'FALLTHROUGH' - return !!flag[:trackEventsFallthrough] - end - false - end - - private def context_to_context_kind(user) - if !user.nil? && user[:anonymous] - return "anonymousUser" - else - return "user" - end - end - end - end -end diff --git a/lib/ldclient-rb/impl/event_summarizer.rb b/lib/ldclient-rb/impl/event_summarizer.rb new file mode 100644 index 00000000..5c9dcc1a --- /dev/null +++ b/lib/ldclient-rb/impl/event_summarizer.rb @@ -0,0 +1,63 @@ +require "ldclient-rb/impl/event_types" + +module LaunchDarkly + module Impl + EventSummary = Struct.new(:start_date, :end_date, :counters) + + EventSummaryFlagInfo = Struct.new(:default, :versions) + + EventSummaryFlagVariationCounter = Struct.new(:value, :count) + + # Manages the state of summarizable information for the EventProcessor, including the + # event counters and user deduplication. Note that the methods of this class are + # deliberately not thread-safe; the EventProcessor is responsible for enforcing + # synchronization across both the summarizer and the event queue. + class EventSummarizer + class Counter + end + + def initialize + clear + end + + # Adds this event to our counters, if it is a type of event we need to count. + def summarize_event(event) + return if !event.is_a?(LaunchDarkly::Impl::EvalEvent) + + counters_for_flag = @counters[event.key] + if counters_for_flag.nil? + counters_for_flag = EventSummaryFlagInfo.new(event.default, Hash.new) + @counters[event.key] = counters_for_flag + end + counters_for_flag_version = counters_for_flag.versions[event.version] + if counters_for_flag_version.nil? + counters_for_flag_version = Hash.new + counters_for_flag.versions[event.version] = counters_for_flag_version + end + variation_counter = counters_for_flag_version[event.variation] + if variation_counter.nil? + counters_for_flag_version[event.variation] = EventSummaryFlagVariationCounter.new(event.value, 1) + else + variation_counter.count = variation_counter.count + 1 + end + time = event.timestamp + if !time.nil? + @start_date = time if @start_date == 0 || time < @start_date + @end_date = time if time > @end_date + end + end + + # Returns a snapshot of the current summarized event data, and resets this state. + def snapshot + ret = EventSummary.new(@start_date, @end_date, @counters) + ret + end + + def clear + @start_date = 0 + @end_date = 0 + @counters = {} + end + end + end +end diff --git a/lib/ldclient-rb/impl/event_types.rb b/lib/ldclient-rb/impl/event_types.rb new file mode 100644 index 00000000..6ca043ba --- /dev/null +++ b/lib/ldclient-rb/impl/event_types.rb @@ -0,0 +1,90 @@ +module LaunchDarkly + module Impl + class Event + def initialize(timestamp, user) + @timestamp = timestamp + @user = user + end + + attr_reader :timestamp + attr_reader :kind + attr_reader :user + end + + class EvalEvent < Event + def initialize(timestamp, user, key, version = nil, variation = nil, value = nil, reason = nil, default = nil, + track_events = false, debug_until = nil, prereq_of = nil) + super(timestamp, user) + @key = key + @version = version + @variation = variation + @value = value + @reason = reason + @default = default + # avoid setting rarely-used attributes if they have no value - this saves a little space per instance + @track_events = track_events if track_events + @debug_until = debug_until if debug_until + @prereq_of = prereq_of if prereq_of + end + + attr_reader :key + attr_reader :version + attr_reader :variation + attr_reader :value + attr_reader :reason + attr_reader :default + attr_reader :track_events + attr_reader :debug_until + attr_reader :prereq_of + end + + class IdentifyEvent < Event + def initialize(timestamp, user) + super(timestamp, user) + end + end + + class CustomEvent < Event + def initialize(timestamp, user, key, data = nil, metric_value = nil) + super(timestamp, user) + @key = key + @data = data if !data.nil? + @metric_value = metric_value if !metric_value.nil? + end + + attr_reader :key + attr_reader :data + attr_reader :metric_value + end + + class AliasEvent < Event + def initialize(timestamp, key, context_kind, previous_key, previous_context_kind) + super(timestamp, nil) + @key = key + @context_kind = context_kind + @previous_key = previous_key + @previous_context_kind = previous_context_kind + end + + attr_reader :key + attr_reader :context_kind + attr_reader :previous_key + attr_reader :previous_context_kind + end + + class IndexEvent < Event + def initialize(timestamp, user) + super(timestamp, user) + end + end + + class DebugEvent < Event + def initialize(eval_event) + super(eval_event.timestamp, eval_event.user) + @eval_event = eval_event + end + + attr_reader :eval_event + end + end +end diff --git a/lib/ldclient-rb/ldclient.rb b/lib/ldclient-rb/ldclient.rb index b5e5ead9..70dc6210 100644 --- a/lib/ldclient-rb/ldclient.rb +++ b/lib/ldclient-rb/ldclient.rb @@ -1,7 +1,6 @@ require "ldclient-rb/impl/big_segments" require "ldclient-rb/impl/diagnostic_events" require "ldclient-rb/impl/evaluator" -require "ldclient-rb/impl/event_factory" require "ldclient-rb/impl/store_client_wrapper" require "concurrent/atomics" require "digest/sha1" @@ -46,9 +45,6 @@ def initialize(sdk_key, config = Config.default, wait_for_sec = 5) @sdk_key = sdk_key - @event_factory_default = EventFactory.new(false) - @event_factory_with_reasons = EventFactory.new(true) - # We need to wrap the feature store object with a FeatureStoreClientWrapper in order to add # some necessary logic around updates. Unfortunately, we have code elsewhere that accesses # the feature store through the Config object, so we need to make a new Config that uses @@ -202,7 +198,7 @@ def initialized? # @return the variation to show the user, or the default value if there's an an error # def variation(key, user, default) - evaluate_internal(key, user, default, @event_factory_default).value + evaluate_internal(key, user, default, false).value end # @@ -229,7 +225,7 @@ def variation(key, user, default) # @return [EvaluationDetail] an object describing the result # def variation_detail(key, user, default) - evaluate_internal(key, user, default, @event_factory_with_reasons) + evaluate_internal(key, user, default, true) end # @@ -253,7 +249,7 @@ def identify(user) return end sanitize_user(user) - @event_processor.add_event(@event_factory_default.new_identify_event(user)) + @event_processor.record_identify_event(user) end # @@ -284,7 +280,7 @@ def track(event_name, user, data = nil, metric_value = nil) return end sanitize_user(user) - @event_processor.add_event(@event_factory_default.new_custom_event(event_name, user, data, metric_value)) + @event_processor.record_custom_event(user, event_name, data, metric_value) end # @@ -301,7 +297,7 @@ def alias(current_context, previous_context) end sanitize_user(current_context) sanitize_user(previous_context) - @event_processor.add_event(@event_factory_default.new_alias_event(current_context, previous_context)) + @event_processor.record_alias_event(current_context, previous_context) end # @@ -368,13 +364,13 @@ def all_flags_state(user, options={}) next end begin - detail = @evaluator.evaluate(f, user, @event_factory_default).detail + detail = @evaluator.evaluate(f, user).detail rescue => exn detail = EvaluationDetail.new(nil, nil, EvaluationReason::error(EvaluationReason::ERROR_EXCEPTION)) Util.log_exception(@config.logger, "Error evaluating flag \"#{k}\" in all_flags_state", exn) end - requires_experiment_data = EventFactory.is_experiment(f, detail.reason) + requires_experiment_data = is_experiment(f, detail.reason) flag_state = { key: f[:key], value: detail.value, @@ -430,7 +426,7 @@ def create_default_data_source(sdk_key, config, diagnostic_accumulator) end # @return [EvaluationDetail] - def evaluate_internal(key, user, default, event_factory) + def evaluate_internal(key, user, default, with_reasons) if @config.offline? return Evaluator.error_result(EvaluationReason::ERROR_CLIENT_NOT_READY, default) end @@ -453,7 +449,7 @@ def evaluate_internal(key, user, default, event_factory) else @config.logger.error { "[LDClient] Client has not finished initializing; feature store unavailable, returning default value" } detail = Evaluator.error_result(EvaluationReason::ERROR_CLIENT_NOT_READY, default) - @event_processor.add_event(event_factory.new_unknown_flag_event(key, user, default, detail.reason)) + record_unknown_flag_eval(key, user, default, detail.reason, with_reasons) return detail end end @@ -463,32 +459,94 @@ def evaluate_internal(key, user, default, event_factory) if feature.nil? @config.logger.info { "[LDClient] Unknown feature flag \"#{key}\". Returning default value" } detail = Evaluator.error_result(EvaluationReason::ERROR_FLAG_NOT_FOUND, default) - @event_processor.add_event(event_factory.new_unknown_flag_event(key, user, default, detail.reason)) + record_unknown_flag_eval(key, user, default, detail.reason, with_reasons) return detail end begin - res = @evaluator.evaluate(feature, user, event_factory) - if !res.events.nil? - res.events.each do |event| - @event_processor.add_event(event) + res = @evaluator.evaluate(feature, user) + if !res.prereq_evals.nil? + res.prereq_evals.each do |prereq_eval| + record_prereq_flag_eval(prereq_eval.prereq_flag, prereq_eval.prereq_of_flag, user, prereq_eval.detail, with_reasons) end end detail = res.detail if detail.default_value? detail = EvaluationDetail.new(default, nil, detail.reason) end - @event_processor.add_event(event_factory.new_eval_event(feature, user, detail, default)) + record_flag_eval(feature, user, detail, default, with_reasons) return detail rescue => exn Util.log_exception(@config.logger, "Error evaluating feature flag \"#{key}\"", exn) detail = Evaluator.error_result(EvaluationReason::ERROR_EXCEPTION, default) - @event_processor.add_event(event_factory.new_default_event(feature, user, default, detail.reason)) + record_flag_eval_error(feature, user, default, detail.reason, with_reasons) return detail end end - def sanitize_user(user) + private def record_flag_eval(flag, user, detail, default, with_reasons) + add_experiment_data = is_experiment(flag, detail.reason) + @event_processor.record_eval_event( + user, + flag[:key], + flag[:version], + detail.variation_index, + detail.value, + (add_experiment_data || with_reasons) ? detail.reason : nil, + default, + add_experiment_data || flag[:trackEvents] || false, + flag[:debugEventsUntilDate], + nil + ) + end + + private def record_prereq_flag_eval(prereq_flag, prereq_of_flag, user, detail, with_reasons) + add_experiment_data = is_experiment(prereq_flag, detail.reason) + @event_processor.record_eval_event( + user, + prereq_flag[:key], + prereq_flag[:version], + detail.variation_index, + detail.value, + (add_experiment_data || with_reasons) ? detail.reason : nil, + nil, + add_experiment_data || prereq_flag[:trackEvents] || false, + prereq_flag[:debugEventsUntilDate], + prereq_of_flag[:key] + ) + end + + private def record_flag_eval_error(flag, user, default, reason, with_reasons) + @event_processor.record_eval_event(user, flag[:key], flag[:version], nil, default, with_reasons ? reason : nil, default, + flag[:trackEvents], flag[:debugEventsUntilDate], nil) + end + + private def record_unknown_flag_eval(flag_key, user, default, reason, with_reasons) + @event_processor.record_eval_event(user, flag_key, nil, nil, default, with_reasons ? reason : nil, default, + false, nil, nil) + end + + private def is_experiment(flag, reason) + return false if !reason + + if reason.in_experiment + return true + end + + case reason[:kind] + when 'RULE_MATCH' + index = reason[:ruleIndex] + if !index.nil? + rules = flag[:rules] || [] + return index >= 0 && index < rules.length && rules[index][:trackEvents] + end + when 'FALLTHROUGH' + return !!flag[:trackEventsFallthrough] + end + false + end + + private def sanitize_user(user) if user[:key] user[:key] = user[:key].to_s end diff --git a/spec/event_summarizer_spec.rb b/spec/event_summarizer_spec.rb deleted file mode 100644 index 5449e691..00000000 --- a/spec/event_summarizer_spec.rb +++ /dev/null @@ -1,63 +0,0 @@ -require "spec_helper" - -describe LaunchDarkly::EventSummarizer do - subject { LaunchDarkly::EventSummarizer } - - let(:user) { { key: "key" } } - - it "does not add identify event to summary" do - es = subject.new - snapshot = es.snapshot - es.summarize_event({ kind: "identify", user: user }) - - expect(es.snapshot).to eq snapshot - end - - it "does not add custom event to summary" do - es = subject.new - snapshot = es.snapshot - es.summarize_event({ kind: "custom", key: "whatever", user: user }) - - expect(es.snapshot).to eq snapshot - end - - it "tracks start and end dates" do - es = subject.new - flag = { key: "key" } - event1 = { kind: "feature", creationDate: 2000, user: user } - event2 = { kind: "feature", creationDate: 1000, user: user } - event3 = { kind: "feature", creationDate: 1500, user: user } - es.summarize_event(event1) - es.summarize_event(event2) - es.summarize_event(event3) - data = es.snapshot - - expect(data.start_date).to be 1000 - expect(data.end_date).to be 2000 - end - - it "counts events" do - es = subject.new - flag1 = { key: "key1", version: 11 } - flag2 = { key: "key2", version: 22 } - event1 = { kind: "feature", key: "key1", version: 11, user: user, variation: 1, value: "value1", default: "default1" } - event2 = { kind: "feature", key: "key1", version: 11, user: user, variation: 2, value: "value2", default: "default1" } - event3 = { kind: "feature", key: "key2", version: 22, user: user, variation: 1, value: "value99", default: "default2" } - event4 = { kind: "feature", key: "key1", version: 11, user: user, variation: 1, value: "value1", default: "default1" } - event5 = { kind: "feature", key: "badkey", user: user, variation: nil, value: "default3", default: "default3" } - [event1, event2, event3, event4, event5].each { |e| es.summarize_event(e) } - data = es.snapshot - - expectedCounters = { - { key: "key1", version: 11, variation: 1 } => - { count: 2, value: "value1", default: "default1" }, - { key: "key1", version: 11, variation: 2 } => - { count: 1, value: "value2", default: "default1" }, - { key: "key2", version: 22, variation: 1 } => - { count: 1, value: "value99", default: "default2" }, - { key: "badkey", version: nil, variation: nil } => - { count: 1, value: "default3", default: "default3" } - } - expect(data.counters).to eq expectedCounters - end -end diff --git a/spec/events_spec.rb b/spec/events_spec.rb index e9a6d6ff..894c3f70 100644 --- a/spec/events_spec.rb +++ b/spec/events_spec.rb @@ -1,3 +1,6 @@ +require "ldclient-rb/impl/event_types" + +require "events_test_util" require "http_util" require "spec_helper" require "time" @@ -5,6 +8,7 @@ describe LaunchDarkly::EventProcessor do subject { LaunchDarkly::EventProcessor } + let(:starting_timestamp) { 1000 } let(:default_config_opts) { { diagnostic_opt_out: true, logger: $null_log } } let(:default_config) { LaunchDarkly::Config.new(default_config_opts) } let(:user) { { key: "userkey", name: "Red" } } @@ -16,7 +20,15 @@ def with_processor_and_sender(config) sender = FakeEventSender.new - ep = subject.new("sdk_key", config, nil, nil, { event_sender: sender }) + timestamp = starting_timestamp + ep = subject.new("sdk_key", config, nil, nil, { + event_sender: sender, + timestamp_fn: proc { + t = timestamp + timestamp += 1 + t + } + }) begin yield ep, sender ensure @@ -26,59 +38,41 @@ def with_processor_and_sender(config) it "queues identify event" do with_processor_and_sender(default_config) do |ep, sender| - e = { kind: "identify", key: user[:key], user: user } - ep.add_event(e) + ep.record_identify_event(user) output = flush_and_get_events(ep, sender) - expect(output).to contain_exactly(e) + expect(output).to contain_exactly(eq(identify_event(user))) end end it "filters user in identify event" do config = LaunchDarkly::Config.new(default_config_opts.merge(all_attributes_private: true)) with_processor_and_sender(config) do |ep, sender| - e = { kind: "identify", key: user[:key], user: user } - ep.add_event(e) + ep.record_identify_event(user) output = flush_and_get_events(ep, sender) - expect(output).to contain_exactly({ - kind: "identify", - key: user[:key], - creationDate: e[:creationDate], - user: filtered_user - }) + expect(output).to contain_exactly(eq(identify_event(filtered_user))) end end it "stringifies built-in user attributes in identify event" do with_processor_and_sender(default_config) do |ep, sender| - flag = { key: "flagkey", version: 11 } - e = { kind: "identify", key: numeric_user[:key], user: numeric_user } - ep.add_event(e) + ep.record_identify_event(numeric_user) output = flush_and_get_events(ep, sender) - expect(output).to contain_exactly( - kind: "identify", - key: numeric_user[:key].to_s, - creationDate: e[:creationDate], - user: stringified_numeric_user - ) + expect(output).to contain_exactly(eq(identify_event(stringified_numeric_user))) end end it "queues individual feature event with index event" do with_processor_and_sender(default_config) do |ep, sender| flag = { key: "flagkey", version: 11 } - fe = { - kind: "feature", key: "flagkey", version: 11, user: user, - variation: 1, value: "value", trackEvents: true - } - ep.add_event(fe) + ep.record_eval_event(user, 'flagkey', 11, 1, 'value', nil, nil, true) output = flush_and_get_events(ep, sender) expect(output).to contain_exactly( - eq(index_event(fe, user)), - eq(feature_event(fe, flag, false, nil)), + eq(index_event(user)), + eq(feature_event(flag, user, 1, 'value')), include(:kind => "summary") ) end @@ -88,16 +82,12 @@ def with_processor_and_sender(config) config = LaunchDarkly::Config.new(default_config_opts.merge(all_attributes_private: true)) with_processor_and_sender(config) do |ep, sender| flag = { key: "flagkey", version: 11 } - fe = { - kind: "feature", key: "flagkey", version: 11, user: user, - variation: 1, value: "value", trackEvents: true - } - ep.add_event(fe) + ep.record_eval_event(user, 'flagkey', 11, 1, 'value', nil, nil, true) output = flush_and_get_events(ep, sender) expect(output).to contain_exactly( - eq(index_event(fe, filtered_user)), - eq(feature_event(fe, flag, false, nil)), + eq(index_event(filtered_user)), + eq(feature_event(flag, user, 1, 'value')), include(:kind => "summary") ) end @@ -106,16 +96,12 @@ def with_processor_and_sender(config) it "stringifies built-in user attributes in index event" do with_processor_and_sender(default_config) do |ep, sender| flag = { key: "flagkey", version: 11 } - fe = { - kind: "feature", key: "flagkey", version: 11, user: numeric_user, - variation: 1, value: "value", trackEvents: true - } - ep.add_event(fe) + ep.record_eval_event(numeric_user, 'flagkey', 11, 1, 'value', nil, nil, true) output = flush_and_get_events(ep, sender) expect(output).to contain_exactly( - eq(index_event(fe, stringified_numeric_user)), - eq(feature_event(fe, flag, false, nil)), + eq(index_event(stringified_numeric_user)), + eq(feature_event(flag, stringified_numeric_user, 1, 'value')), include(:kind => "summary") ) end @@ -125,15 +111,11 @@ def with_processor_and_sender(config) config = LaunchDarkly::Config.new(default_config_opts.merge(inline_users_in_events: true)) with_processor_and_sender(config) do |ep, sender| flag = { key: "flagkey", version: 11 } - fe = { - kind: "feature", key: "flagkey", version: 11, user: user, - variation: 1, value: "value", trackEvents: true - } - ep.add_event(fe) + ep.record_eval_event(user, 'flagkey', 11, 1, 'value', nil, nil, true) output = flush_and_get_events(ep, sender) expect(output).to contain_exactly( - eq(feature_event(fe, flag, false, user)), + eq(feature_event(flag, user, 1, 'value', true)), include(:kind => "summary") ) end @@ -143,15 +125,11 @@ def with_processor_and_sender(config) config = LaunchDarkly::Config.new(default_config_opts.merge(inline_users_in_events: true)) with_processor_and_sender(config) do |ep, sender| flag = { key: "flagkey", version: 11 } - fe = { - kind: "feature", key: "flagkey", version: 11, user: numeric_user, - variation: 1, value: "value", trackEvents: true - } - ep.add_event(fe) + ep.record_eval_event(numeric_user, 'flagkey', 11, 1, 'value', nil, nil, true) output = flush_and_get_events(ep, sender) expect(output).to contain_exactly( - eq(feature_event(fe, flag, false, stringified_numeric_user)), + eq(feature_event(flag, stringified_numeric_user, 1, 'value', true)), include(:kind => "summary") ) end @@ -161,15 +139,11 @@ def with_processor_and_sender(config) config = LaunchDarkly::Config.new(default_config_opts.merge(all_attributes_private: true, inline_users_in_events: true)) with_processor_and_sender(config) do |ep, sender| flag = { key: "flagkey", version: 11 } - fe = { - kind: "feature", key: "flagkey", version: 11, user: user, - variation: 1, value: "value", trackEvents: true - } - ep.add_event(fe) + ep.record_eval_event(user, 'flagkey', 11, 1, 'value', nil, nil, true) output = flush_and_get_events(ep, sender) expect(output).to contain_exactly( - eq(feature_event(fe, flag, false, filtered_user)), + eq(feature_event(flag, filtered_user, 1, 'value', true)), include(:kind => "summary") ) end @@ -179,15 +153,11 @@ def with_processor_and_sender(config) config = LaunchDarkly::Config.new(default_config_opts.merge(inline_users_in_events: true)) with_processor_and_sender(config) do |ep, sender| flag = { key: "flagkey", version: 11 } - fe = { - kind: "feature", key: "flagkey", version: 11, user: user, - variation: 1, value: "value", trackEvents: false - } - ep.add_event(fe) + ep.record_eval_event(user, 'flagkey', 11, 1, 'value', nil, nil, false) output = flush_and_get_events(ep, sender) expect(output).to contain_exactly( - eq(index_event(fe, user)), + eq(index_event(user)), include(:kind => "summary") ) end @@ -197,16 +167,12 @@ def with_processor_and_sender(config) with_processor_and_sender(default_config) do |ep, sender| flag = { key: "flagkey", version: 11 } future_time = (Time.now.to_f * 1000).to_i + 1000000 - fe = { - kind: "feature", key: "flagkey", version: 11, user: user, - variation: 1, value: "value", trackEvents: false, debugEventsUntilDate: future_time - } - ep.add_event(fe) + ep.record_eval_event(user, 'flagkey', 11, 1, 'value', nil, nil, false, future_time) output = flush_and_get_events(ep, sender) expect(output).to contain_exactly( - eq(index_event(fe, user)), - eq(feature_event(fe, flag, true, user)), + eq(index_event(user)), + eq(debug_event(flag, user, 1, 'value')), include(:kind => "summary") ) end @@ -216,17 +182,13 @@ def with_processor_and_sender(config) with_processor_and_sender(default_config) do |ep, sender| flag = { key: "flagkey", version: 11 } future_time = (Time.now.to_f * 1000).to_i + 1000000 - fe = { - kind: "feature", key: "flagkey", version: 11, user: user, - variation: 1, value: "value", trackEvents: true, debugEventsUntilDate: future_time - } - ep.add_event(fe) + ep.record_eval_event(user, 'flagkey', 11, 1, 'value', nil, nil, true, future_time) output = flush_and_get_events(ep, sender) expect(output).to contain_exactly( - eq(index_event(fe, user)), - eq(feature_event(fe, flag, false, nil)), - eq(feature_event(fe, flag, true, user)), + eq(index_event(user)), + eq(feature_event(flag, user, 1, 'value')), + eq(debug_event(flag, user, 1, 'value')), include(:kind => "summary") ) end @@ -239,18 +201,15 @@ def with_processor_and_sender(config) # Send and flush an event we don't care about, just to set the last server time sender.result = LaunchDarkly::Impl::EventSenderResult.new(true, false, server_time) - ep.add_event({ kind: "identify", user: user }) + + ep.record_identify_event(user) flush_and_get_events(ep, sender) # Now send an event with debug mode on, with a "debug until" time that is further in # the future than the server time, but in the past compared to the client. flag = { key: "flagkey", version: 11 } debug_until = (server_time.to_f * 1000).to_i + 1000 - fe = { - kind: "feature", key: "flagkey", version: 11, user: user, - variation: 1, value: "value", trackEvents: false, debugEventsUntilDate: debug_until - } - ep.add_event(fe) + ep.record_eval_event(user, 'flagkey', 11, 1, 'value', nil, nil, false, debug_until) # Should get a summary event only, not a full feature event output = flush_and_get_events(ep, sender) @@ -267,18 +226,14 @@ def with_processor_and_sender(config) # Send and flush an event we don't care about, just to set the last server time sender.result = LaunchDarkly::Impl::EventSenderResult.new(true, false, server_time) - ep.add_event({ kind: "identify", user: user }) + ep.record_identify_event(user) flush_and_get_events(ep, sender) # Now send an event with debug mode on, with a "debug until" time that is further in # the future than the server time, but in the past compared to the client. flag = { key: "flagkey", version: 11 } debug_until = (server_time.to_f * 1000).to_i - 1000 - fe = { - kind: "feature", key: "flagkey", version: 11, user: user, - variation: 1, value: "value", trackEvents: false, debugEventsUntilDate: debug_until - } - ep.add_event(fe) + ep.record_eval_event(user, 'flagkey', 11, 1, 'value', nil, nil, false, debug_until) # Should get a summary event only, not a full feature event output = flush_and_get_events(ep, sender) @@ -293,22 +248,14 @@ def with_processor_and_sender(config) flag1 = { key: "flagkey1", version: 11 } flag2 = { key: "flagkey2", version: 22 } future_time = (Time.now.to_f * 1000).to_i + 1000000 - fe1 = { - kind: "feature", key: "flagkey1", version: 11, user: user, - variation: 1, value: "value", trackEvents: true - } - fe2 = { - kind: "feature", key: "flagkey2", version: 22, user: user, - variation: 1, value: "value", trackEvents: true - } - ep.add_event(fe1) - ep.add_event(fe2) + ep.record_eval_event(user, 'flagkey1', 11, 1, 'value', nil, nil, true) + ep.record_eval_event(user, 'flagkey2', 22, 1, 'value', nil, nil, true) output = flush_and_get_events(ep, sender) expect(output).to contain_exactly( - eq(index_event(fe1, user)), - eq(feature_event(fe1, flag1, false, nil)), - eq(feature_event(fe2, flag2, false, nil)), + eq(index_event(user)), + eq(feature_event(flag1, user, 1, 'value', false, starting_timestamp)), + eq(feature_event(flag2, user, 1, 'value', false, starting_timestamp + 1)), include(:kind => "summary") ) end @@ -319,24 +266,16 @@ def with_processor_and_sender(config) flag1 = { key: "flagkey1", version: 11 } flag2 = { key: "flagkey2", version: 22 } future_time = (Time.now.to_f * 1000).to_i + 1000000 - fe1 = { - kind: "feature", key: "flagkey1", version: 11, user: user, - variation: 1, value: "value1", default: "default1" - } - fe2 = { - kind: "feature", key: "flagkey2", version: 22, user: user, - variation: 2, value: "value2", default: "default2" - } - ep.add_event(fe1) - ep.add_event(fe2) + ep.record_eval_event(user, 'flagkey1', 11, 1, 'value1', nil, 'default1', false) + ep.record_eval_event(user, 'flagkey2', 22, 2, 'value2', nil, 'default2', false) output = flush_and_get_events(ep, sender) expect(output).to contain_exactly( - eq(index_event(fe1, user)), + eq(index_event(user)), eq({ kind: "summary", - startDate: fe1[:creationDate], - endDate: fe2[:creationDate], + startDate: starting_timestamp, + endDate: starting_timestamp + 1, features: { flagkey1: { default: "default1", @@ -358,13 +297,12 @@ def with_processor_and_sender(config) it "queues custom event with user" do with_processor_and_sender(default_config) do |ep, sender| - e = { kind: "custom", key: "eventkey", user: user, data: { thing: "stuff" }, metricValue: 1.5 } - ep.add_event(e) + ep.record_custom_event(user, 'eventkey', { thing: 'stuff' }, 1.5) output = flush_and_get_events(ep, sender) expect(output).to contain_exactly( - eq(index_event(e, user)), - eq(custom_event(e, nil)) + eq(index_event(user)), + eq(custom_event(user, 'eventkey', { thing: 'stuff' }, 1.5)) ) end end @@ -372,12 +310,11 @@ def with_processor_and_sender(config) it "can include inline user in custom event" do config = LaunchDarkly::Config.new(default_config_opts.merge(inline_users_in_events: true)) with_processor_and_sender(config) do |ep, sender| - e = { kind: "custom", key: "eventkey", user: user, data: { thing: "stuff" } } - ep.add_event(e) + ep.record_custom_event(user, 'eventkey') output = flush_and_get_events(ep, sender) expect(output).to contain_exactly( - eq(custom_event(e, user)) + eq(custom_event(user, 'eventkey', nil, nil, true)) ) end end @@ -385,12 +322,11 @@ def with_processor_and_sender(config) it "filters user in custom event" do config = LaunchDarkly::Config.new(default_config_opts.merge(all_attributes_private: true, inline_users_in_events: true)) with_processor_and_sender(config) do |ep, sender| - e = { kind: "custom", key: "eventkey", user: user, data: { thing: "stuff" } } - ep.add_event(e) + ep.record_custom_event(user, 'eventkey') output = flush_and_get_events(ep, sender) expect(output).to contain_exactly( - eq(custom_event(e, filtered_user)) + eq(custom_event(filtered_user, 'eventkey', nil, nil, true)) ) end end @@ -398,46 +334,49 @@ def with_processor_and_sender(config) it "stringifies built-in user attributes in custom event" do config = LaunchDarkly::Config.new(default_config_opts.merge(inline_users_in_events: true)) with_processor_and_sender(config) do |ep, sender| - e = { kind: "custom", key: "eventkey", user: numeric_user } - ep.add_event(e) + ep.record_custom_event(numeric_user, 'eventkey', nil, nil) output = flush_and_get_events(ep, sender) expect(output).to contain_exactly( - eq(custom_event(e, stringified_numeric_user)) + eq(custom_event(stringified_numeric_user, 'eventkey', nil, nil, true)) ) end end it "queues alias event" do with_processor_and_sender(default_config) do |ep, sender| - e = { kind: "alias", key: "a", contextKind: "user", previousKey: "b", previousContextKind: "user" } - ep.add_event(e) + ep.record_alias_event({ key: 'a' }, { key: 'b', anonymous: true }) output = flush_and_get_events(ep, sender) - expect(output).to contain_exactly(e) + expect(output).to contain_exactly({ + creationDate: starting_timestamp, + kind: 'alias', + key: 'a', + contextKind: 'user', + previousKey: 'b', + previousContextKind: 'anonymousUser' + }) end end it "treats nil value for custom the same as an empty hash" do with_processor_and_sender(default_config) do |ep, sender| user_with_nil_custom = { key: "userkey", custom: nil } - e = { kind: "identify", key: "userkey", user: user_with_nil_custom } - ep.add_event(e) + ep.record_identify_event(user_with_nil_custom) output = flush_and_get_events(ep, sender) - expect(output).to contain_exactly(e) + expect(output).to contain_exactly(eq(identify_event(user_with_nil_custom))) end end it "does a final flush when shutting down" do with_processor_and_sender(default_config) do |ep, sender| - e = { kind: "identify", key: user[:key], user: user } - ep.add_event(e) + ep.record_identify_event(user) ep.stop output = sender.analytics_payloads.pop - expect(output).to contain_exactly(e) + expect(output).to contain_exactly(eq(identify_event(user))) end end @@ -452,12 +391,10 @@ def with_processor_and_sender(config) it "stops posting events after unrecoverable error" do with_processor_and_sender(default_config) do |ep, sender| sender.result = LaunchDarkly::Impl::EventSenderResult.new(false, true, nil) - e = { kind: "identify", key: user[:key], user: user } - ep.add_event(e) + e = ep.record_identify_event(user) flush_and_get_events(ep, sender) - e = { kind: "identify", key: user[:key], user: user } - ep.add_event(e) + ep.record_identify_event(user) ep.flush ep.wait_until_inactive expect(sender.analytics_payloads.empty?).to be true @@ -510,9 +447,9 @@ def with_diagnostic_processor_and_sender(config) with_diagnostic_processor_and_sender(config) do |ep, sender| init_event = sender.diagnostic_payloads.pop - ep.add_event({ kind: 'identify', user: user }) - ep.add_event({ kind: 'identify', user: user }) - ep.add_event({ kind: 'identify', user: user }) + 3.times do + ep.record_identify_event(user) + end flush_and_get_events(ep, sender) periodic_event = sender.diagnostic_payloads.pop @@ -528,8 +465,8 @@ def with_diagnostic_processor_and_sender(config) with_diagnostic_processor_and_sender(diagnostic_config) do |ep, sender| init_event = sender.diagnostic_payloads.pop - ep.add_event({ kind: 'custom', key: 'event1', user: user }) - ep.add_event({ kind: 'custom', key: 'event2', user: user }) + ep.record_custom_event(user, 'event1') + ep.record_custom_event(user, 'event2') events = flush_and_get_events(ep, sender) periodic_event = sender.diagnostic_payloads.pop @@ -541,44 +478,66 @@ def with_diagnostic_processor_and_sender(config) end end - def index_event(e, user) + def index_event(user, timestamp = starting_timestamp) { kind: "index", - creationDate: e[:creationDate], + creationDate: timestamp, user: user } end - def feature_event(e, flag, debug, inline_user) + def identify_event(user, timestamp = starting_timestamp) + { + kind: "identify", + creationDate: timestamp, + key: user[:key], + user: user + } + end + + def feature_event(flag, user, variation, value, inline_user = false, timestamp = starting_timestamp) out = { - kind: debug ? "debug" : "feature", - creationDate: e[:creationDate], + kind: 'feature', + creationDate: timestamp, key: flag[:key], - variation: e[:variation], + variation: variation, version: flag[:version], - value: e[:value] + value: value } - if inline_user.nil? - out[:userKey] = e[:user][:key] + if inline_user + out[:user] = user else - out[:user] = inline_user + out[:userKey] = user[:key] end out end - def custom_event(e, inline_user) + def debug_event(flag, user, variation, value, timestamp = starting_timestamp) + out = { + kind: 'debug', + creationDate: timestamp, + key: flag[:key], + variation: variation, + version: flag[:version], + value: value, + user: user + } + out + end + + def custom_event(user, key, data, metric_value, inline_user = false, timestamp = starting_timestamp) out = { kind: "custom", - creationDate: e[:creationDate], - key: e[:key] + creationDate: timestamp, + key: key } - out[:data] = e[:data] if e.has_key?(:data) - if inline_user.nil? - out[:userKey] = e[:user][:key] + out[:data] = data if !data.nil? + if inline_user + out[:user] = user else - out[:user] = inline_user + out[:userKey] = user[:key] end - out[:metricValue] = e[:metricValue] if e.has_key?(:metricValue) + out[:metricValue] = metric_value if !metric_value.nil? out end diff --git a/spec/events_test_util.rb b/spec/events_test_util.rb new file mode 100644 index 00000000..66b5b97d --- /dev/null +++ b/spec/events_test_util.rb @@ -0,0 +1,19 @@ +require "ldclient-rb/impl/event_types" + +def make_eval_event(timestamp, user, key, version = nil, variation = nil, value = nil, reason = nil, + default = nil, track_events = false, debug_until = nil, prereq_of = nil) + LaunchDarkly::Impl::EvalEvent.new(timestamp, user, key, version, variation, value, reason, + default, track_events, debug_until, prereq_of) +end + +def make_identify_event(timestamp, user) + LaunchDarkly::Impl::IdentifyEvent.new(timestamp, user) +end + +def make_custom_event(timestamp, user, key, data = nil, metric_value = nil) + LaunchDarkly::Impl::CustomEvent.new(timestamp, user, key, data, metric_value) +end + +def make_alias_event(timestamp, key, context_kind, previous_key, previous_context_kind) + LaunchDarkly::Impl::AliasEvent.new(timestamp, key, context_kind, previous_key, previous_context_kind) +end diff --git a/spec/impl/evaluator_big_segments_spec.rb b/spec/impl/evaluator_big_segments_spec.rb index b8a9e2e4..32db7d79 100644 --- a/spec/impl/evaluator_big_segments_spec.rb +++ b/spec/impl/evaluator_big_segments_spec.rb @@ -20,7 +20,7 @@ module Impl with_segment(segment). build flag = boolean_flag_with_clauses([make_segment_match_clause(segment)]) - result = e.evaluate(flag, user, factory) + result = e.evaluate(flag, user) expect(result.detail.value).to be false expect(result.detail.reason.big_segments_status).to be(BigSegmentsStatus::NOT_CONFIGURED) end @@ -36,7 +36,7 @@ module Impl with_segment(segment). build flag = boolean_flag_with_clauses([make_segment_match_clause(segment)]) - result = e.evaluate(flag, user, factory) + result = e.evaluate(flag, user) expect(result.detail.value).to be false expect(result.detail.reason.big_segments_status).to be(BigSegmentsStatus::NOT_CONFIGURED) end @@ -53,7 +53,7 @@ module Impl with_big_segment_for_user(user, segment, true). build flag = boolean_flag_with_clauses([make_segment_match_clause(segment)]) - result = e.evaluate(flag, user, factory) + result = e.evaluate(flag, user) expect(result.detail.value).to be true expect(result.detail.reason.big_segments_status).to be(BigSegmentsStatus::HEALTHY) end @@ -73,7 +73,7 @@ module Impl with_big_segment_for_user(user, segment, nil). build flag = boolean_flag_with_clauses([make_segment_match_clause(segment)]) - result = e.evaluate(flag, user, factory) + result = e.evaluate(flag, user) expect(result.detail.value).to be true expect(result.detail.reason.big_segments_status).to be(BigSegmentsStatus::HEALTHY) end @@ -93,7 +93,7 @@ module Impl with_big_segment_for_user(user, segment, false). build flag = boolean_flag_with_clauses([make_segment_match_clause(segment)]) - result = e.evaluate(flag, user, factory) + result = e.evaluate(flag, user) expect(result.detail.value).to be false expect(result.detail.reason.big_segments_status).to be(BigSegmentsStatus::HEALTHY) end @@ -111,7 +111,7 @@ module Impl with_big_segments_status(BigSegmentsStatus::STALE). build flag = boolean_flag_with_clauses([make_segment_match_clause(segment)]) - result = e.evaluate(flag, user, factory) + result = e.evaluate(flag, user) expect(result.detail.value).to be true expect(result.detail.reason.big_segments_status).to be(BigSegmentsStatus::STALE) end @@ -149,7 +149,7 @@ module Impl # The membership deliberately does not include segment1, because we want the first rule to be # a non-match so that it will continue on and check segment2 as well. - result = e.evaluate(flag, user, factory) + result = e.evaluate(flag, user) expect(result.detail.value).to be true expect(result.detail.reason.big_segments_status).to be(BigSegmentsStatus::HEALTHY) diff --git a/spec/impl/evaluator_clause_spec.rb b/spec/impl/evaluator_clause_spec.rb index a90a5499..2b76505d 100644 --- a/spec/impl/evaluator_clause_spec.rb +++ b/spec/impl/evaluator_clause_spec.rb @@ -10,28 +10,28 @@ module Impl user = { key: 'x', name: 'Bob' } clause = { attribute: 'name', op: 'in', values: ['Bob'] } flag = boolean_flag_with_clauses([clause]) - expect(basic_evaluator.evaluate(flag, user, factory).detail.value).to be true + expect(basic_evaluator.evaluate(flag, user).detail.value).to be true end it "can match custom attribute" do user = { key: 'x', name: 'Bob', custom: { legs: 4 } } clause = { attribute: 'legs', op: 'in', values: [4] } flag = boolean_flag_with_clauses([clause]) - expect(basic_evaluator.evaluate(flag, user, factory).detail.value).to be true + expect(basic_evaluator.evaluate(flag, user).detail.value).to be true end it "returns false for missing attribute" do user = { key: 'x', name: 'Bob' } clause = { attribute: 'legs', op: 'in', values: [4] } flag = boolean_flag_with_clauses([clause]) - expect(basic_evaluator.evaluate(flag, user, factory).detail.value).to be false + expect(basic_evaluator.evaluate(flag, user).detail.value).to be false end it "returns false for unknown operator" do user = { key: 'x', name: 'Bob' } clause = { attribute: 'name', op: 'unknown', values: [4] } flag = boolean_flag_with_clauses([clause]) - expect(basic_evaluator.evaluate(flag, user, factory).detail.value).to be false + expect(basic_evaluator.evaluate(flag, user).detail.value).to be false end it "does not stop evaluating rules after clause with unknown operator" do @@ -41,14 +41,14 @@ module Impl clause1 = { attribute: 'name', op: 'in', values: ['Bob'] } rule1 = { clauses: [ clause1 ], variation: 1 } flag = boolean_flag_with_rules([rule0, rule1]) - expect(basic_evaluator.evaluate(flag, user, factory).detail.value).to be true + expect(basic_evaluator.evaluate(flag, user).detail.value).to be true end it "can be negated" do user = { key: 'x', name: 'Bob' } clause = { attribute: 'name', op: 'in', values: ['Bob'], negate: true } flag = boolean_flag_with_clauses([clause]) - expect(basic_evaluator.evaluate(flag, user, factory).detail.value).to be false + expect(basic_evaluator.evaluate(flag, user).detail.value).to be false end end end diff --git a/spec/impl/evaluator_rule_spec.rb b/spec/impl/evaluator_rule_spec.rb index 7299decb..6a6b9310 100644 --- a/spec/impl/evaluator_rule_spec.rb +++ b/spec/impl/evaluator_rule_spec.rb @@ -11,9 +11,9 @@ module Impl flag = boolean_flag_with_rules([rule]) user = { key: 'userkey' } detail = EvaluationDetail.new(true, 1, EvaluationReason::rule_match(0, 'ruleid')) - result = basic_evaluator.evaluate(flag, user, factory) + result = basic_evaluator.evaluate(flag, user) expect(result.detail).to eq(detail) - expect(result.events).to eq(nil) + expect(result.prereq_evals).to eq(nil) end it "reuses rule match reason instances if possible" do @@ -22,8 +22,8 @@ module Impl Model.postprocess_item_after_deserializing!(FEATURES, flag) # now there's a cached rule match reason user = { key: 'userkey' } detail = EvaluationDetail.new(true, 1, EvaluationReason::rule_match(0, 'ruleid')) - result1 = basic_evaluator.evaluate(flag, user, factory) - result2 = basic_evaluator.evaluate(flag, user, factory) + result1 = basic_evaluator.evaluate(flag, user) + result2 = basic_evaluator.evaluate(flag, user) expect(result1.detail.reason.rule_id).to eq 'ruleid' expect(result1.detail.reason).to be result2.detail.reason end @@ -34,9 +34,9 @@ module Impl user = { key: 'userkey' } detail = EvaluationDetail.new(nil, nil, EvaluationReason::error(EvaluationReason::ERROR_MALFORMED_FLAG)) - result = basic_evaluator.evaluate(flag, user, factory) + result = basic_evaluator.evaluate(flag, user) expect(result.detail).to eq(detail) - expect(result.events).to eq(nil) + expect(result.prereq_evals).to eq(nil) end it "returns an error if rule variation is negative" do @@ -45,9 +45,9 @@ module Impl user = { key: 'userkey' } detail = EvaluationDetail.new(nil, nil, EvaluationReason::error(EvaluationReason::ERROR_MALFORMED_FLAG)) - result = basic_evaluator.evaluate(flag, user, factory) + result = basic_evaluator.evaluate(flag, user) expect(result.detail).to eq(detail) - expect(result.events).to eq(nil) + expect(result.prereq_evals).to eq(nil) end it "returns an error if rule has neither variation nor rollout" do @@ -56,9 +56,9 @@ module Impl user = { key: 'userkey' } detail = EvaluationDetail.new(nil, nil, EvaluationReason::error(EvaluationReason::ERROR_MALFORMED_FLAG)) - result = basic_evaluator.evaluate(flag, user, factory) + result = basic_evaluator.evaluate(flag, user) expect(result.detail).to eq(detail) - expect(result.events).to eq(nil) + expect(result.prereq_evals).to eq(nil) end it "returns an error if rule has a rollout with no variations" do @@ -68,16 +68,16 @@ module Impl user = { key: 'userkey' } detail = EvaluationDetail.new(nil, nil, EvaluationReason::error(EvaluationReason::ERROR_MALFORMED_FLAG)) - result = basic_evaluator.evaluate(flag, user, factory) + result = basic_evaluator.evaluate(flag, user) expect(result.detail).to eq(detail) - expect(result.events).to eq(nil) + expect(result.prereq_evals).to eq(nil) end it "coerces user key to a string for evaluation" do clause = { attribute: 'key', op: 'in', values: ['999'] } flag = boolean_flag_with_clauses([clause]) user = { key: 999 } - result = basic_evaluator.evaluate(flag, user, factory) + result = basic_evaluator.evaluate(flag, user) expect(result.detail.value).to eq(true) end @@ -88,7 +88,7 @@ module Impl rollout: { salt: '', variations: [ { weight: 100000, variation: 1 } ] } } flag = boolean_flag_with_rules([rule]) user = { key: "userkey", secondary: 999 } - result = basic_evaluator.evaluate(flag, user, factory) + result = basic_evaluator.evaluate(flag, user) expect(result.detail.reason).to eq(EvaluationReason::rule_match(0, 'ruleid')) end @@ -98,7 +98,7 @@ module Impl rollout: { kind: 'experiment', variations: [ { weight: 100000, variation: 1, untracked: false } ] } } flag = boolean_flag_with_rules([rule]) user = { key: "userkey", secondary: 999 } - result = basic_evaluator.evaluate(flag, user, factory) + result = basic_evaluator.evaluate(flag, user) expect(result.detail.reason.to_json).to include('"inExperiment":true') expect(result.detail.reason.in_experiment).to eq(true) end @@ -108,7 +108,7 @@ module Impl rollout: { kind: 'rollout', variations: [ { weight: 100000, variation: 1, untracked: false } ] } } flag = boolean_flag_with_rules([rule]) user = { key: "userkey", secondary: 999 } - result = basic_evaluator.evaluate(flag, user, factory) + result = basic_evaluator.evaluate(flag, user) expect(result.detail.reason.to_json).to_not include('"inExperiment":true') expect(result.detail.reason.in_experiment).to eq(nil) end @@ -118,7 +118,7 @@ module Impl rollout: { kind: 'experiment', variations: [ { weight: 100000, variation: 1, untracked: true } ] } } flag = boolean_flag_with_rules([rule]) user = { key: "userkey", secondary: 999 } - result = basic_evaluator.evaluate(flag, user, factory) + result = basic_evaluator.evaluate(flag, user) expect(result.detail.reason.to_json).to_not include('"inExperiment":true') expect(result.detail.reason.in_experiment).to eq(nil) end diff --git a/spec/impl/evaluator_segment_spec.rb b/spec/impl/evaluator_segment_spec.rb index 5cd85148..bb526b7c 100644 --- a/spec/impl/evaluator_segment_spec.rb +++ b/spec/impl/evaluator_segment_spec.rb @@ -10,7 +10,7 @@ def test_segment_match(segment) clause = make_segment_match_clause(segment) flag = boolean_flag_with_clauses([clause]) e = EvaluatorBuilder.new(logger).with_segment(segment).build - e.evaluate(flag, user, factory).detail.value + e.evaluate(flag, user).detail.value end it "retrieves segment from segment store for segmentMatch operator" do @@ -22,14 +22,14 @@ def test_segment_match(segment) } e = EvaluatorBuilder.new(logger).with_segment(segment).build flag = boolean_flag_with_clauses([make_segment_match_clause(segment)]) - expect(e.evaluate(flag, user, factory).detail.value).to be true + expect(e.evaluate(flag, user).detail.value).to be true end it "falls through with no errors if referenced segment is not found" do e = EvaluatorBuilder.new(logger).with_unknown_segment('segkey').build clause = { attribute: '', op: 'segmentMatch', values: ['segkey'] } flag = boolean_flag_with_clauses([clause]) - expect(e.evaluate(flag, user, factory).detail.value).to be false + expect(e.evaluate(flag, user).detail.value).to be false end it 'explicitly includes user' do diff --git a/spec/impl/evaluator_spec.rb b/spec/impl/evaluator_spec.rb index 15766866..20b231fb 100644 --- a/spec/impl/evaluator_spec.rb +++ b/spec/impl/evaluator_spec.rb @@ -1,3 +1,4 @@ +require "events_test_util" require "spec_helper" require "impl/evaluator_spec_base" @@ -17,9 +18,9 @@ module Impl } user = { key: 'x' } detail = EvaluationDetail.new('b', 1, EvaluationReason::off) - result = basic_evaluator.evaluate(flag, user, factory) + result = basic_evaluator.evaluate(flag, user) expect(result.detail).to eq(detail) - expect(result.events).to eq(nil) + expect(result.prereq_evals).to eq(nil) end it "returns nil if flag is off and off variation is unspecified" do @@ -31,9 +32,9 @@ module Impl } user = { key: 'x' } detail = EvaluationDetail.new(nil, nil, EvaluationReason::off) - result = basic_evaluator.evaluate(flag, user, factory) + result = basic_evaluator.evaluate(flag, user) expect(result.detail).to eq(detail) - expect(result.events).to eq(nil) + expect(result.prereq_evals).to eq(nil) end it "returns an error if off variation is too high" do @@ -47,9 +48,9 @@ module Impl user = { key: 'x' } detail = EvaluationDetail.new(nil, nil, EvaluationReason::error(EvaluationReason::ERROR_MALFORMED_FLAG)) - result = basic_evaluator.evaluate(flag, user, factory) + result = basic_evaluator.evaluate(flag, user) expect(result.detail).to eq(detail) - expect(result.events).to eq(nil) + expect(result.prereq_evals).to eq(nil) end it "returns an error if off variation is negative" do @@ -63,9 +64,9 @@ module Impl user = { key: 'x' } detail = EvaluationDetail.new(nil, nil, EvaluationReason::error(EvaluationReason::ERROR_MALFORMED_FLAG)) - result = basic_evaluator.evaluate(flag, user, factory) + result = basic_evaluator.evaluate(flag, user) expect(result.detail).to eq(detail) - expect(result.events).to eq(nil) + expect(result.prereq_evals).to eq(nil) end it "returns off variation if prerequisite is not found" do @@ -80,9 +81,9 @@ module Impl user = { key: 'x' } detail = EvaluationDetail.new('b', 1, EvaluationReason::prerequisite_failed('badfeature')) e = EvaluatorBuilder.new(logger).with_unknown_flag('badfeature').build - result = e.evaluate(flag, user, factory) + result = e.evaluate(flag, user) expect(result.detail).to eq(detail) - expect(result.events).to eq(nil) + expect(result.prereq_evals).to eq(nil) end it "reuses prerequisite-failed reason instances if possible" do @@ -97,9 +98,9 @@ module Impl Model.postprocess_item_after_deserializing!(FEATURES, flag) # now there's a cached reason user = { key: 'x' } e = EvaluatorBuilder.new(logger).with_unknown_flag('badfeature').build - result1 = e.evaluate(flag, user, factory) + result1 = e.evaluate(flag, user) expect(result1.detail.reason).to eq EvaluationReason::prerequisite_failed('badfeature') - result2 = e.evaluate(flag, user, factory) + result2 = e.evaluate(flag, user) expect(result2.detail.reason).to be result1.detail.reason end @@ -123,13 +124,13 @@ module Impl } user = { key: 'x' } detail = EvaluationDetail.new('b', 1, EvaluationReason::prerequisite_failed('feature1')) - events_should_be = [{ - kind: 'feature', key: 'feature1', user: user, value: nil, default: nil, variation: nil, version: 2, prereqOf: 'feature0' - }] + expected_prereqs = [ + PrerequisiteEvalRecord.new(flag1, flag, EvaluationDetail.new(nil, nil, EvaluationReason::prerequisite_failed('feature2'))) + ] e = EvaluatorBuilder.new(logger).with_flag(flag1).with_unknown_flag('feature2').build - result = e.evaluate(flag, user, factory) + result = e.evaluate(flag, user) expect(result.detail).to eq(detail) - expect(result.events).to eq(events_should_be) + expect(result.prereq_evals).to eq(expected_prereqs) end it "returns off variation and event if prerequisite is off" do @@ -153,13 +154,13 @@ module Impl } user = { key: 'x' } detail = EvaluationDetail.new('b', 1, EvaluationReason::prerequisite_failed('feature1')) - events_should_be = [{ - kind: 'feature', key: 'feature1', user: user, variation: 1, value: 'e', default: nil, version: 2, prereqOf: 'feature0' - }] + expected_prereqs = [ + PrerequisiteEvalRecord.new(flag1, flag, EvaluationDetail.new('e', 1, EvaluationReason::off)) + ] e = EvaluatorBuilder.new(logger).with_flag(flag1).build - result = e.evaluate(flag, user, factory) + result = e.evaluate(flag, user) expect(result.detail).to eq(detail) - expect(result.events).to eq(events_should_be) + expect(result.prereq_evals).to eq(expected_prereqs) end it "returns off variation and event if prerequisite is not met" do @@ -181,13 +182,13 @@ module Impl } user = { key: 'x' } detail = EvaluationDetail.new('b', 1, EvaluationReason::prerequisite_failed('feature1')) - events_should_be = [{ - kind: 'feature', key: 'feature1', user: user, variation: 0, value: 'd', default: nil, version: 2, prereqOf: 'feature0' - }] + expected_prereqs = [ + PrerequisiteEvalRecord.new(flag1, flag, EvaluationDetail.new('d', 0, EvaluationReason::fallthrough)) + ] e = EvaluatorBuilder.new(logger).with_flag(flag1).build - result = e.evaluate(flag, user, factory) + result = e.evaluate(flag, user) expect(result.detail).to eq(detail) - expect(result.events).to eq(events_should_be) + expect(result.prereq_evals).to eq(expected_prereqs) end it "returns fallthrough variation and event if prerequisite is met and there are no rules" do @@ -209,13 +210,13 @@ module Impl } user = { key: 'x' } detail = EvaluationDetail.new('a', 0, EvaluationReason::fallthrough) - events_should_be = [{ - kind: 'feature', key: 'feature1', user: user, variation: 1, value: 'e', default: nil, version: 2, prereqOf: 'feature0' - }] + expected_prereqs = [ + PrerequisiteEvalRecord.new(flag1, flag, EvaluationDetail.new('e', 1, EvaluationReason::fallthrough)) + ] e = EvaluatorBuilder.new(logger).with_flag(flag1).build - result = e.evaluate(flag, user, factory) + result = e.evaluate(flag, user) expect(result.detail).to eq(detail) - expect(result.events).to eq(events_should_be) + expect(result.prereq_evals).to eq(expected_prereqs) end it "returns an error if fallthrough variation is too high" do @@ -228,9 +229,9 @@ module Impl } user = { key: 'userkey' } detail = EvaluationDetail.new(nil, nil, EvaluationReason::error(EvaluationReason::ERROR_MALFORMED_FLAG)) - result = basic_evaluator.evaluate(flag, user, factory) + result = basic_evaluator.evaluate(flag, user) expect(result.detail).to eq(detail) - expect(result.events).to eq(nil) + expect(result.prereq_evals).to eq(nil) end it "returns an error if fallthrough variation is negative" do @@ -243,9 +244,9 @@ module Impl } user = { key: 'userkey' } detail = EvaluationDetail.new(nil, nil, EvaluationReason::error(EvaluationReason::ERROR_MALFORMED_FLAG)) - result = basic_evaluator.evaluate(flag, user, factory) + result = basic_evaluator.evaluate(flag, user) expect(result.detail).to eq(detail) - expect(result.events).to eq(nil) + expect(result.prereq_evals).to eq(nil) end it "returns an error if fallthrough has no variation or rollout" do @@ -258,9 +259,9 @@ module Impl } user = { key: 'userkey' } detail = EvaluationDetail.new(nil, nil, EvaluationReason::error(EvaluationReason::ERROR_MALFORMED_FLAG)) - result = basic_evaluator.evaluate(flag, user, factory) + result = basic_evaluator.evaluate(flag, user) expect(result.detail).to eq(detail) - expect(result.events).to eq(nil) + expect(result.prereq_evals).to eq(nil) end it "returns an error if fallthrough has a rollout with no variations" do @@ -273,9 +274,9 @@ module Impl } user = { key: 'userkey' } detail = EvaluationDetail.new(nil, nil, EvaluationReason::error(EvaluationReason::ERROR_MALFORMED_FLAG)) - result = basic_evaluator.evaluate(flag, user, factory) + result = basic_evaluator.evaluate(flag, user) expect(result.detail).to eq(detail) - expect(result.events).to eq(nil) + expect(result.prereq_evals).to eq(nil) end it "matches user from targets" do @@ -291,9 +292,9 @@ module Impl } user = { key: 'userkey' } detail = EvaluationDetail.new('c', 2, EvaluationReason::target_match) - result = basic_evaluator.evaluate(flag, user, factory) + result = basic_evaluator.evaluate(flag, user) expect(result.detail).to eq(detail) - expect(result.events).to eq(nil) + expect(result.prereq_evals).to eq(nil) end describe "experiment rollout behavior" do @@ -306,7 +307,7 @@ module Impl variations: ['a', 'b', 'c'] } user = { key: 'userkey' } - result = basic_evaluator.evaluate(flag, user, factory) + result = basic_evaluator.evaluate(flag, user) expect(result.detail.reason.to_json).to include('"inExperiment":true') expect(result.detail.reason.in_experiment).to eq(true) end @@ -320,7 +321,7 @@ module Impl variations: ['a', 'b', 'c'] } user = { key: 'userkey' } - result = basic_evaluator.evaluate(flag, user, factory) + result = basic_evaluator.evaluate(flag, user) expect(result.detail.reason.to_json).to_not include('"inExperiment":true') expect(result.detail.reason.in_experiment).to eq(nil) end @@ -334,7 +335,7 @@ module Impl variations: ['a', 'b', 'c'] } user = { key: 'userkey' } - result = basic_evaluator.evaluate(flag, user, factory) + result = basic_evaluator.evaluate(flag, user) expect(result.detail.reason.to_json).to_not include('"inExperiment":true') expect(result.detail.reason.in_experiment).to eq(nil) end diff --git a/spec/impl/evaluator_spec_base.rb b/spec/impl/evaluator_spec_base.rb index da8662ac..6008c8b9 100644 --- a/spec/impl/evaluator_spec_base.rb +++ b/spec/impl/evaluator_spec_base.rb @@ -75,10 +75,6 @@ def build end module EvaluatorSpecBase - def factory - EventFactory.new(false) - end - def user { key: "userkey", diff --git a/spec/impl/event_factory_spec.rb b/spec/impl/event_factory_spec.rb deleted file mode 100644 index 9da19de0..00000000 --- a/spec/impl/event_factory_spec.rb +++ /dev/null @@ -1,108 +0,0 @@ -require "spec_helper" - -describe LaunchDarkly::Impl::EventFactory do - subject { LaunchDarkly::Impl::EventFactory } - - describe "#new_eval_event" do - let(:event_factory_without_reason) { subject.new(false) } - let(:user) { { 'key': 'userA' } } - let(:rule_with_experiment_rollout) { - { id: 'ruleid', - clauses: [{ attribute: 'key', op: 'in', values: ['userkey'] }], - trackEvents: false, - rollout: { kind: 'experiment', salt: '', variations: [ { weight: 100000, variation: 0, untracked: false } ] } - } - } - - let(:rule_with_rollout) { - { id: 'ruleid', - trackEvents: false, - clauses: [{ attribute: 'key', op: 'in', values: ['userkey'] }], - rollout: { salt: '', variations: [ { weight: 100000, variation: 0, untracked: false } ] } - } - } - - let(:fallthrough_with_rollout) { - { rollout: { kind: 'rollout', salt: '', variations: [ { weight: 100000, variation: 0, untracked: false } ], trackEventsFallthrough: false } } - } - - let(:rule_reason) { LaunchDarkly::EvaluationReason::rule_match(0, 'ruleid') } - let(:rule_reason_with_experiment) { LaunchDarkly::EvaluationReason::rule_match(0, 'ruleid', true) } - let(:fallthrough_reason) { LaunchDarkly::EvaluationReason::fallthrough } - let(:fallthrough_reason_with_experiment) { LaunchDarkly::EvaluationReason::fallthrough(true) } - - context "in_experiment is true" do - it "sets the reason and trackevents: true for rules" do - flag = createFlag('rule', rule_with_experiment_rollout) - detail = LaunchDarkly::EvaluationDetail.new(true, 0, rule_reason_with_experiment) - r = subject.new(false).new_eval_event(flag, user, detail, nil, nil) - expect(r[:trackEvents]).to eql(true) - expect(r[:reason].to_s).to eql("RULE_MATCH(0,ruleid,true)") - end - - it "sets the reason and trackevents: true for the fallthrough" do - fallthrough_with_rollout[:kind] = 'experiment' - flag = createFlag('fallthrough', fallthrough_with_rollout) - detail = LaunchDarkly::EvaluationDetail.new(true, 0, fallthrough_reason_with_experiment) - r = subject.new(false).new_eval_event(flag, user, detail, nil, nil) - expect(r[:trackEvents]).to eql(true) - expect(r[:reason].to_s).to eql("FALLTHROUGH(true)") - end - end - - context "in_experiment is false" do - it "sets the reason & trackEvents: true if rule has trackEvents set to true" do - rule_with_rollout[:trackEvents] = true - flag = createFlag('rule', rule_with_rollout) - detail = LaunchDarkly::EvaluationDetail.new(true, 0, rule_reason) - r = subject.new(false).new_eval_event(flag, user, detail, nil, nil) - expect(r[:trackEvents]).to eql(true) - expect(r[:reason].to_s).to eql("RULE_MATCH(0,ruleid)") - end - - it "sets the reason & trackEvents: true if fallthrough has trackEventsFallthrough set to true" do - flag = createFlag('fallthrough', fallthrough_with_rollout) - flag[:trackEventsFallthrough] = true - detail = LaunchDarkly::EvaluationDetail.new(true, 0, fallthrough_reason) - r = subject.new(false).new_eval_event(flag, user, detail, nil, nil) - expect(r[:trackEvents]).to eql(true) - expect(r[:reason].to_s).to eql("FALLTHROUGH") - end - - it "doesn't set the reason & trackEvents if rule has trackEvents set to false" do - flag = createFlag('rule', rule_with_rollout) - detail = LaunchDarkly::EvaluationDetail.new(true, 0, rule_reason) - r = subject.new(false).new_eval_event(flag, user, detail, nil, nil) - expect(r[:trackEvents]).to be_nil - expect(r[:reason]).to be_nil - end - - it "doesn't set the reason & trackEvents if fallthrough has trackEventsFallthrough set to false" do - flag = createFlag('fallthrough', fallthrough_with_rollout) - detail = LaunchDarkly::EvaluationDetail.new(true, 0, fallthrough_reason) - r = subject.new(false).new_eval_event(flag, user, detail, nil, nil) - expect(r[:trackEvents]).to be_nil - expect(r[:reason]).to be_nil - end - - it "sets trackEvents true and doesn't set the reason if flag[:trackEvents] = true" do - flag = createFlag('fallthrough', fallthrough_with_rollout) - flag[:trackEvents] = true - detail = LaunchDarkly::EvaluationDetail.new(true, 0, fallthrough_reason) - r = subject.new(false).new_eval_event(flag, user, detail, nil, nil) - expect(r[:trackEvents]).to eql(true) - expect(r[:reason]).to be_nil - end - end - end - - def createFlag(kind, rule) - if kind == 'rule' - { key: 'feature', on: true, rules: [rule], fallthrough: { variation: 0 }, variations: [ false, true ] } - elsif kind == 'fallthrough' - { key: 'feature', on: true, fallthrough: rule, variations: [ false, true ] } - else - { key: 'feature', on: true, fallthrough: { variation: 0 }, variations: [ false, true ] } - end - end -end \ No newline at end of file diff --git a/spec/impl/event_summarizer_spec.rb b/spec/impl/event_summarizer_spec.rb new file mode 100644 index 00000000..bbd3f2ba --- /dev/null +++ b/spec/impl/event_summarizer_spec.rb @@ -0,0 +1,84 @@ +require "ldclient-rb/impl/event_types" + +require "events_test_util" +require "spec_helper" + +module LaunchDarkly + module Impl + describe EventSummarizer do + subject { EventSummarizer } + + let(:user) { { key: "key" } } + + it "does not add identify event to summary" do + es = subject.new + snapshot = es.snapshot + es.summarize_event({ kind: "identify", user: user }) + + expect(es.snapshot).to eq snapshot + end + + it "does not add custom event to summary" do + es = subject.new + snapshot = es.snapshot + es.summarize_event({ kind: "custom", key: "whatever", user: user }) + + expect(es.snapshot).to eq snapshot + end + + it "tracks start and end dates" do + es = subject.new + flag = { key: "key" } + event1 = make_eval_event(2000, user, 'flag1') + event2 = make_eval_event(1000, user, 'flag1') + event3 = make_eval_event(1500, user, 'flag1') + es.summarize_event(event1) + es.summarize_event(event2) + es.summarize_event(event3) + data = es.snapshot + + expect(data.start_date).to be 1000 + expect(data.end_date).to be 2000 + end + + it "counts events" do + es = subject.new + flag1 = { key: "key1", version: 11 } + flag2 = { key: "key2", version: 22 } + event1 = make_eval_event(0, user, 'key1', 11, 1, 'value1', nil, 'default1') + event2 = make_eval_event(0, user, 'key1', 11, 2, 'value2', nil, 'default1') + event3 = make_eval_event(0, user, 'key2', 22, 1, 'value99', nil, 'default2') + event4 = make_eval_event(0, user, 'key1', 11, 1, 'value99', nil, 'default1') + event5 = make_eval_event(0, user, 'badkey', nil, nil, 'default3', nil, 'default3') + [event1, event2, event3, event4, event5].each { |e| es.summarize_event(e) } + data = es.snapshot + + expectedCounters = { + 'key1' => EventSummaryFlagInfo.new( + 'default1', { + 11 => { + 1 => EventSummaryFlagVariationCounter.new('value1', 2), + 2 => EventSummaryFlagVariationCounter.new('value2', 1) + } + } + ), + 'key2' => EventSummaryFlagInfo.new( + 'default2', { + 22 => { + 1 => EventSummaryFlagVariationCounter.new('value99', 1) + } + } + ), + 'badkey' => EventSummaryFlagInfo.new( + 'default3', { + nil => { + nil => EventSummaryFlagVariationCounter.new('default3', 1) + } + } + ) + } + expect(data.counters).to eq expectedCounters + end + end + end +end diff --git a/spec/ldclient_events_spec.rb b/spec/ldclient_events_spec.rb index b2afcc13..ba82617b 100644 --- a/spec/ldclient_events_spec.rb +++ b/spec/ldclient_events_spec.rb @@ -1,5 +1,6 @@ require "ldclient-rb" +require "events_test_util" require "mock_components" require "model_builders" require "spec_helper" @@ -19,9 +20,9 @@ def event_processor(client) context "evaluation events - variation" do it "unknown flag" do with_client(test_config) do |client| - expect(event_processor(client)).to receive(:add_event).with(hash_including( - kind: "feature", key: "badkey", user: basic_user, value: "default", default: "default" - )) + expect(event_processor(client)).to receive(:record_eval_event).with( + basic_user, 'badkey', nil, nil, 'default', nil, 'default', false, nil, nil + ) client.variation("badkey", basic_user, "default") end end @@ -31,15 +32,9 @@ def event_processor(client) td.update(td.flag("flagkey").variations("value").variation_for_all_users(0)) with_client(test_config(data_source: td)) do |client| - expect(event_processor(client)).to receive(:add_event).with(hash_including( - kind: "feature", - key: "flagkey", - version: 1, - user: basic_user, - variation: 0, - value: "value", - default: "default" - )) + expect(event_processor(client)).to receive(:record_eval_event).with( + basic_user, 'flagkey', 1, 0, 'value', nil, 'default', false, nil, nil + ) client.variation("flagkey", basic_user, "default") end end @@ -51,7 +46,7 @@ def event_processor(client) logger = double().as_null_object with_client(test_config(data_source: td, logger: logger)) do |client| - expect(event_processor(client)).not_to receive(:add_event) + expect(event_processor(client)).not_to receive(:record_eval_event) expect(logger).to receive(:error) client.variation("flagkey", nil, "default") end @@ -65,7 +60,7 @@ def event_processor(client) keyless_user = { key: nil } with_client(test_config(data_source: td, logger: logger)) do |client| - expect(event_processor(client)).not_to receive(:add_event) + expect(event_processor(client)).not_to receive(:record_eval_event) expect(logger).to receive(:warn) client.variation("flagkey", keyless_user, "default") end @@ -81,17 +76,10 @@ def event_processor(client) ) with_client(test_config(data_source: td)) do |client| - expect(event_processor(client)).to receive(:add_event).with(hash_including( - kind: "feature", - key: "flagkey", - version: 100, - user: basic_user, - variation: 0, - value: "value", - default: "default", - trackEvents: true, - reason: LaunchDarkly::EvaluationReason::rule_match(0, 'id') - )) + expect(event_processor(client)).to receive(:record_eval_event).with( + basic_user, 'flagkey', 100, 0, 'value', LaunchDarkly::EvaluationReason::rule_match(0, 'id'), + 'default', true, nil, nil + ) client.variation("flagkey", basic_user, "default") end end @@ -104,17 +92,10 @@ def event_processor(client) ) with_client(test_config(data_source: td)) do |client| - expect(event_processor(client)).to receive(:add_event).with(hash_including( - kind: "feature", - key: "flagkey", - version: 100, - user: basic_user, - variation: 0, - value: "value", - default: "default", - trackEvents: true, - reason: LaunchDarkly::EvaluationReason::fallthrough - )) + expect(event_processor(client)).to receive(:record_eval_event).with( + basic_user, 'flagkey', 100, 0, 'value', LaunchDarkly::EvaluationReason::fallthrough, + 'default', true, nil, nil + ) client.variation("flagkey", basic_user, "default") end end @@ -123,10 +104,11 @@ def event_processor(client) context "evaluation events - variation_detail" do it "unknown flag" do with_client(test_config) do |client| - expect(event_processor(client)).to receive(:add_event).with(hash_including( - kind: "feature", key: "badkey", user: basic_user, value: "default", default: "default", - reason: LaunchDarkly::EvaluationReason::error(LaunchDarkly::EvaluationReason::ERROR_FLAG_NOT_FOUND) - )) + expect(event_processor(client)).to receive(:record_eval_event).with( + basic_user, 'badkey', nil, nil, 'default', + LaunchDarkly::EvaluationReason::error(LaunchDarkly::EvaluationReason::ERROR_FLAG_NOT_FOUND), + 'default', false, nil, nil + ) client.variation_detail("badkey", basic_user, "default") end end @@ -136,16 +118,10 @@ def event_processor(client) td.update(td.flag("flagkey").variations("value").on(false).off_variation(0)) with_client(test_config(data_source: td)) do |client| - expect(event_processor(client)).to receive(:add_event).with(hash_including( - kind: "feature", - key: "flagkey", - version: 1, - user: basic_user, - variation: 0, - value: "value", - default: "default", - reason: LaunchDarkly::EvaluationReason::off - )) + expect(event_processor(client)).to receive(:record_eval_event).with( + basic_user, 'flagkey', 1, 0, 'value', LaunchDarkly::EvaluationReason::off, + 'default', false, nil, nil + ) client.variation_detail("flagkey", basic_user, "default") end end @@ -157,7 +133,7 @@ def event_processor(client) logger = double().as_null_object with_client(test_config(data_source: td, logger: logger)) do |client| - expect(event_processor(client)).not_to receive(:add_event) + expect(event_processor(client)).not_to receive(:record_eval_event) expect(logger).to receive(:error) client.variation_detail("flagkey", nil, "default") end @@ -170,7 +146,7 @@ def event_processor(client) logger = double().as_null_object with_client(test_config(data_source: td, logger: logger)) do |client| - expect(event_processor(client)).not_to receive(:add_event) + expect(event_processor(client)).not_to receive(:record_eval_event) expect(logger).to receive(:warn) client.variation_detail("flagkey", { key: nil }, "default") end @@ -180,8 +156,7 @@ def event_processor(client) context "identify" do it "queues up an identify event" do with_client(test_config) do |client| - expect(event_processor(client)).to receive(:add_event).with(hash_including( - kind: "identify", key: basic_user[:key], user: basic_user)) + expect(event_processor(client)).to receive(:record_identify_event).with(basic_user) client.identify(basic_user) end end @@ -190,7 +165,7 @@ def event_processor(client) logger = double().as_null_object with_client(test_config(logger: logger)) do |client| - expect(event_processor(client)).not_to receive(:add_event) + expect(event_processor(client)).not_to receive(:record_identify_event) expect(logger).to receive(:warn) client.identify(nil) end @@ -200,7 +175,7 @@ def event_processor(client) logger = double().as_null_object with_client(test_config(logger: logger)) do |client| - expect(event_processor(client)).not_to receive(:add_event) + expect(event_processor(client)).not_to receive(:record_identify_event) expect(logger).to receive(:warn) client.identify({ key: "" }) end @@ -210,36 +185,30 @@ def event_processor(client) context "track" do it "queues up an custom event" do with_client(test_config) do |client| - expect(event_processor(client)).to receive(:add_event).with(hash_including( - kind: "custom", key: "custom_event_name", user: basic_user, data: 42)) + expect(event_processor(client)).to receive(:record_custom_event).with( + basic_user, 'custom_event_name', 42, nil + ) client.track("custom_event_name", basic_user, 42) end end it "can include a metric value" do with_client(test_config) do |client| - expect(event_processor(client)).to receive(:add_event).with(hash_including( - kind: "custom", key: "custom_event_name", user: basic_user, metricValue: 1.5)) + expect(event_processor(client)).to receive(:record_custom_event).with( + basic_user, 'custom_event_name', nil, 1.5 + ) client.track("custom_event_name", basic_user, nil, 1.5) end end - it "includes contextKind with anonymous user" do - anon_user = { key: 'user-key', anonymous: true } - - with_client(test_config) do |client| - expect(event_processor(client)).to receive(:add_event).with(hash_including( - kind: "custom", key: "custom_event_name", user: anon_user, metricValue: 2.2, contextKind: "anonymousUser")) - client.track("custom_event_name", anon_user, nil, 2.2) - end - end - it "sanitizes the user in the event" do numeric_key_user = { key: 33 } sanitized_user = { key: "33" } with_client(test_config) do |client| - expect(event_processor(client)).to receive(:add_event).with(hash_including(user: sanitized_user)) + expect(event_processor(client)).to receive(:record_custom_event).with( + sanitized_user, 'custom_event_name', nil, nil + ) client.track("custom_event_name", numeric_key_user, nil) end end @@ -248,7 +217,7 @@ def event_processor(client) logger = double().as_null_object with_client(test_config(logger: logger)) do |client| - expect(event_processor(client)).not_to receive(:add_event) + expect(event_processor(client)).not_to receive(:record_custom_event) expect(logger).to receive(:warn) client.track("custom_event_name", nil, nil) end @@ -258,7 +227,7 @@ def event_processor(client) logger = double().as_null_object with_client(test_config(logger: logger)) do |client| - expect(event_processor(client)).not_to receive(:add_event) + expect(event_processor(client)).not_to receive(:record_custom_event) expect(logger).to receive(:warn) client.track("custom_event_name", { key: nil }, nil) end @@ -270,8 +239,7 @@ def event_processor(client) anon_user = { key: "user-key", anonymous: true } with_client(test_config) do |client| - expect(event_processor(client)).to receive(:add_event).with(hash_including( - kind: "alias", key: basic_user[:key], contextKind: "user", previousKey: anon_user[:key], previousContextKind: "anonymousUser")) + expect(event_processor(client)).to receive(:record_alias_event).with(basic_user, anon_user) client.alias(basic_user, anon_user) end end @@ -280,7 +248,7 @@ def event_processor(client) logger = double().as_null_object with_client(test_config(logger: logger)) do |client| - expect(event_processor(client)).not_to receive(:add_event) + expect(event_processor(client)).not_to receive(:record_alias_event) expect(logger).to receive(:warn) client.alias(nil, nil) end @@ -290,7 +258,7 @@ def event_processor(client) logger = double().as_null_object with_client(test_config(logger: logger)) do |client| - expect(event_processor(client)).not_to receive(:add_event) + expect(event_processor(client)).not_to receive(:record_alias_event) expect(logger).to receive(:warn) client.alias({ key: nil }, { key: nil }) end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index c54ef444..9b87bc33 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -20,6 +20,9 @@ def ensure_stop(thing) end RSpec.configure do |config| + config.expect_with :rspec do |expectations| + expectations.max_formatted_output_length = 1000 # otherwise rspec tends to abbreviate our failure output and make it unreadable + end config.before(:each) do end end