diff --git a/Gemfile.lock b/Gemfile.lock index 155eccf2..b319f572 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - launchdarkly-server-sdk (5.5.7) + launchdarkly-server-sdk (5.5.12) concurrent-ruby (~> 1.0) json (>= 1.8, < 3) ld-eventsource (= 1.0.1) @@ -35,7 +35,6 @@ GEM ffi (1.9.25) ffi (1.9.25-java) hitimes (1.3.1) - hitimes (1.3.1-java) http_tools (0.4.5) jmespath (1.4.0) json (1.8.6) diff --git a/lib/ldclient-rb/evaluation.rb b/lib/ldclient-rb/evaluation.rb index 7edef6b2..43a03c23 100644 --- a/lib/ldclient-rb/evaluation.rb +++ b/lib/ldclient-rb/evaluation.rb @@ -199,7 +199,7 @@ def error_result(errorKind, value = nil) # Evaluates a feature flag and returns an EvalResult. The result.value will be nil if the flag returns # the default value. Error conditions produce a result with an error reason, not an exception. - def evaluate(flag, user, store, logger) + def evaluate(flag, user, store, logger, event_factory) if user.nil? || user[:key].nil? return EvalResult.new(error_result('USER_NOT_SPECIFIED'), []) end @@ -207,16 +207,16 @@ def evaluate(flag, user, store, logger) sanitized_user = Util.stringify_attrs(user, USER_ATTRS_TO_STRINGIFY_FOR_EVALUATION) events = [] - detail = eval_internal(flag, sanitized_user, store, events, logger) + detail = eval_internal(flag, sanitized_user, store, events, logger, event_factory) return EvalResult.new(detail, events) end - def eval_internal(flag, user, store, events, logger) + def eval_internal(flag, user, store, events, logger, event_factory) if !flag[:on] return get_off_value(flag, { kind: 'OFF' }, logger) end - prereq_failure_reason = check_prerequisites(flag, user, store, events, logger) + prereq_failure_reason = check_prerequisites(flag, user, store, events, logger, event_factory) if !prereq_failure_reason.nil? return get_off_value(flag, prereq_failure_reason, logger) end @@ -249,7 +249,7 @@ def eval_internal(flag, user, store, events, logger) return EvaluationDetail.new(nil, nil, { kind: 'FALLTHROUGH' }) end - def check_prerequisites(flag, user, store, events, logger) + def check_prerequisites(flag, user, store, events, logger, event_factory) (flag[:prerequisites] || []).each do |prerequisite| prereq_ok = true prereq_key = prerequisite[:key] @@ -260,23 +260,13 @@ def check_prerequisites(flag, user, store, events, logger) prereq_ok = false else begin - prereq_res = eval_internal(prereq_flag, user, store, events, logger) + prereq_res = eval_internal(prereq_flag, user, store, events, logger, event_factory) # 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 = { - kind: "feature", - key: prereq_key, - user: user, - variation: prereq_res.variation_index, - value: prereq_res.value, - version: prereq_flag[:version], - prereqOf: flag[:key], - trackEvents: prereq_flag[:trackEvents], - debugEventsUntilDate: prereq_flag[:debugEventsUntilDate] - } + event = event_factory.new_eval_event(prereq_flag, user, prereq_res, nil, flag) events.push(event) rescue => exn Util.log_exception(logger, "Error evaluating prerequisite flag \"#{prereq_key}\" for flag \"#{flag[:key]}\"", exn) diff --git a/lib/ldclient-rb/events.rb b/lib/ldclient-rb/events.rb index f57287a4..184facc4 100644 --- a/lib/ldclient-rb/events.rb +++ b/lib/ldclient-rb/events.rb @@ -456,6 +456,7 @@ def make_output_event(event) else out[:userKey] = event[:user].nil? ? nil : event[:user][:key] end + out[:metricValue] = event[:metricValue] if event.has_key?(:metricValue) out when "index" { diff --git a/lib/ldclient-rb/impl/event_factory.rb b/lib/ldclient-rb/impl/event_factory.rb new file mode 100644 index 00000000..2e7d2697 --- /dev/null +++ b/lib/ldclient-rb/impl/event_factory.rb @@ -0,0 +1,98 @@ + +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 = 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 + 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 + 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 + end + + def new_identify_event(user) + { + kind: 'identify', + key: user[:key], + user: user + } + 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 + end + + private + + def is_experiment(flag, reason) + return false if !reason + 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 + end + end +end diff --git a/lib/ldclient-rb/ldclient.rb b/lib/ldclient-rb/ldclient.rb index 3680619a..b7c2ee85 100644 --- a/lib/ldclient-rb/ldclient.rb +++ b/lib/ldclient-rb/ldclient.rb @@ -1,3 +1,4 @@ +require "ldclient-rb/impl/event_factory" require "ldclient-rb/impl/store_client_wrapper" require "concurrent/atomics" require "digest/sha1" @@ -13,6 +14,7 @@ module LaunchDarkly # class LDClient include Evaluation + include Impl # # Creates a new client instance that connects to LaunchDarkly. A custom # configuration parameter can also supplied to specify advanced options, @@ -32,6 +34,9 @@ class LDClient 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 @@ -165,7 +170,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, false).value + evaluate_internal(key, user, default, @event_factory_default).value end # @@ -192,7 +197,7 @@ def variation(key, user, default) # @return [EvaluationDetail] an object describing the result # def variation_detail(key, user, default) - evaluate_internal(key, user, default, true) + evaluate_internal(key, user, default, @event_factory_with_reasons) end # @@ -215,7 +220,8 @@ def identify(user) @config.logger.warn("Identify called with nil user or nil user key!") return end - @event_processor.add_event(kind: "identify", key: user[:key], user: user) + sanitize_user(user) + @event_processor.add_event(@event_factory_default.new_identify_event(user)) end # @@ -225,18 +231,28 @@ def identify(user) # Note that event delivery is asynchronous, so the event may not actually be sent # until later; see {#flush}. # + # As of this version’s release date, the LaunchDarkly service does not support the `metricValue` + # parameter. As a result, specifying `metricValue` will not yet produce any different behavior + # from omitting it. Refer to the [SDK reference guide](https://docs.launchdarkly.com/docs/ruby-sdk-reference#section-track) + # for the latest status. + # # @param event_name [String] The name of the event # @param user [Hash] The user to register; this can have all the same user properties # described in {#variation} - # @param data [Hash] A hash containing any additional data associated with the event + # @param data [Hash] An optional hash containing any additional data associated with the event + # @param metric_value [Number] A numeric value used by the LaunchDarkly experimentation + # feature in numeric custom metrics. Can be omitted if this event is used by only + # non-numeric metrics. This field will also be returned as part of the custom event + # for Data Export. # @return [void] # - def track(event_name, user, data) + def track(event_name, user, data = nil, metric_value = nil) if !user || user[:key].nil? @config.logger.warn("Track called with nil user or nil user key!") return end - @event_processor.add_event(kind: "custom", key: event_name, user: user, data: data) + sanitize_user(user) + @event_processor.add_event(@event_factory_default.new_custom_event(event_name, user, data, metric_value)) end # @@ -294,7 +310,7 @@ def all_flags_state(user, options={}) next end begin - result = evaluate(f, user, @store, @config.logger) + result = evaluate(f, user, @store, @config.logger, @event_factory_default) state.add_flag(f, result.detail.value, result.detail.variation_index, with_reasons ? result.detail.reason : nil, details_only_if_tracked) rescue => exn @@ -334,7 +350,7 @@ def create_default_data_source(sdk_key, config) end # @return [EvaluationDetail] - def evaluate_internal(key, user, default, include_reasons_in_events) + def evaluate_internal(key, user, default, event_factory) if @config.offline? return error_result('CLIENT_NOT_READY', default) end @@ -344,8 +360,9 @@ def evaluate_internal(key, user, default, include_reasons_in_events) @config.logger.warn { "[LDClient] Client has not finished initializing; using last known values from feature store" } else @config.logger.error { "[LDClient] Client has not finished initializing; feature store unavailable, returning default value" } - @event_processor.add_event(kind: "feature", key: key, value: default, default: default, user: user) - return error_result('CLIENT_NOT_READY', default) + detail = error_result('CLIENT_NOT_READY', default) + @event_processor.add_event(event_factory.new_unknown_flag_event(key, user, default, detail.reason)) + return detail end end @@ -354,20 +371,19 @@ def evaluate_internal(key, user, default, include_reasons_in_events) if feature.nil? @config.logger.info { "[LDClient] Unknown feature flag \"#{key}\". Returning default value" } detail = error_result('FLAG_NOT_FOUND', default) - @event_processor.add_event(kind: "feature", key: key, value: default, default: default, user: user, - reason: include_reasons_in_events ? detail.reason : nil) + @event_processor.add_event(event_factory.new_unknown_flag_event(key, user, default, detail.reason)) return detail end unless user @config.logger.error { "[LDClient] Must specify user" } detail = error_result('USER_NOT_SPECIFIED', default) - @event_processor.add_event(make_feature_event(feature, nil, detail, default, include_reasons_in_events)) + @event_processor.add_event(event_factory.new_default_event(feature, user, default, detail.reason)) return detail end begin - res = evaluate(feature, user, @store, @config.logger) # note, evaluate will do its own sanitization + res = evaluate(feature, user, @store, @config.logger, event_factory) if !res.events.nil? res.events.each do |event| @event_processor.add_event(event) @@ -377,29 +393,20 @@ def evaluate_internal(key, user, default, include_reasons_in_events) if detail.default_value? detail = EvaluationDetail.new(default, nil, detail.reason) end - @event_processor.add_event(make_feature_event(feature, user, detail, default, include_reasons_in_events)) + @event_processor.add_event(event_factory.new_eval_event(feature, user, detail, default)) return detail rescue => exn Util.log_exception(@config.logger, "Error evaluating feature flag \"#{key}\"", exn) detail = error_result('EXCEPTION', default) - @event_processor.add_event(make_feature_event(feature, user, detail, default, include_reasons_in_events)) + @event_processor.add_event(event_factory.new_default_event(feature, user, default, detail.reason)) return detail end end - def make_feature_event(flag, user, detail, default, with_reasons) - { - kind: "feature", - key: flag[:key], - user: user, - variation: detail.variation_index, - value: detail.value, - default: default, - version: flag[:version], - trackEvents: flag[:trackEvents], - debugEventsUntilDate: flag[:debugEventsUntilDate], - reason: with_reasons ? detail.reason : nil - } + def sanitize_user(user) + if user[:key] + user[:key] = user[:key].to_s + end end end diff --git a/spec/evaluation_spec.rb b/spec/evaluation_spec.rb index 52a617b6..ff4b63f6 100644 --- a/spec/evaluation_spec.rb +++ b/spec/evaluation_spec.rb @@ -7,6 +7,8 @@ let(:features) { LaunchDarkly::InMemoryFeatureStore.new } + let(:factory) { LaunchDarkly::Impl::EventFactory.new(false) } + let(:user) { { key: "userkey", @@ -36,7 +38,7 @@ def boolean_flag_with_clauses(clauses) } user = { key: 'x' } detail = LaunchDarkly::EvaluationDetail.new('b', 1, { kind: 'OFF' }) - result = evaluate(flag, user, features, logger) + result = evaluate(flag, user, features, logger, factory) expect(result.detail).to eq(detail) expect(result.events).to eq([]) end @@ -50,7 +52,7 @@ def boolean_flag_with_clauses(clauses) } user = { key: 'x' } detail = LaunchDarkly::EvaluationDetail.new(nil, nil, { kind: 'OFF' }) - result = evaluate(flag, user, features, logger) + result = evaluate(flag, user, features, logger, factory) expect(result.detail).to eq(detail) expect(result.events).to eq([]) end @@ -66,7 +68,7 @@ def boolean_flag_with_clauses(clauses) user = { key: 'x' } detail = LaunchDarkly::EvaluationDetail.new(nil, nil, { kind: 'ERROR', errorKind: 'MALFORMED_FLAG' }) - result = evaluate(flag, user, features, logger) + result = evaluate(flag, user, features, logger, factory) expect(result.detail).to eq(detail) expect(result.events).to eq([]) end @@ -82,7 +84,7 @@ def boolean_flag_with_clauses(clauses) user = { key: 'x' } detail = LaunchDarkly::EvaluationDetail.new(nil, nil, { kind: 'ERROR', errorKind: 'MALFORMED_FLAG' }) - result = evaluate(flag, user, features, logger) + result = evaluate(flag, user, features, logger, factory) expect(result.detail).to eq(detail) expect(result.events).to eq([]) end @@ -99,7 +101,7 @@ def boolean_flag_with_clauses(clauses) user = { key: 'x' } detail = LaunchDarkly::EvaluationDetail.new('b', 1, { kind: 'PREREQUISITE_FAILED', prerequisiteKey: 'badfeature' }) - result = evaluate(flag, user, features, logger) + result = evaluate(flag, user, features, logger, factory) expect(result.detail).to eq(detail) expect(result.events).to eq([]) end @@ -127,10 +129,9 @@ def boolean_flag_with_clauses(clauses) detail = LaunchDarkly::EvaluationDetail.new('b', 1, { kind: 'PREREQUISITE_FAILED', prerequisiteKey: 'feature1' }) events_should_be = [{ - kind: 'feature', key: 'feature1', user: user, variation: nil, value: nil, version: 2, prereqOf: 'feature0', - trackEvents: nil, debugEventsUntilDate: nil + kind: 'feature', key: 'feature1', user: user, value: nil, default: nil, variation: nil, version: 2, prereqOf: 'feature0' }] - result = evaluate(flag, user, features, logger) + result = evaluate(flag, user, features, logger, factory) expect(result.detail).to eq(detail) expect(result.events).to eq(events_should_be) end @@ -159,10 +160,9 @@ def boolean_flag_with_clauses(clauses) detail = LaunchDarkly::EvaluationDetail.new('b', 1, { kind: 'PREREQUISITE_FAILED', prerequisiteKey: 'feature1' }) events_should_be = [{ - kind: 'feature', key: 'feature1', user: user, variation: 1, value: 'e', version: 2, prereqOf: 'feature0', - trackEvents: nil, debugEventsUntilDate: nil + kind: 'feature', key: 'feature1', user: user, variation: 1, value: 'e', default: nil, version: 2, prereqOf: 'feature0' }] - result = evaluate(flag, user, features, logger) + result = evaluate(flag, user, features, logger, factory) expect(result.detail).to eq(detail) expect(result.events).to eq(events_should_be) end @@ -189,10 +189,9 @@ def boolean_flag_with_clauses(clauses) detail = LaunchDarkly::EvaluationDetail.new('b', 1, { kind: 'PREREQUISITE_FAILED', prerequisiteKey: 'feature1' }) events_should_be = [{ - kind: 'feature', key: 'feature1', user: user, variation: 0, value: 'd', version: 2, prereqOf: 'feature0', - trackEvents: nil, debugEventsUntilDate: nil + kind: 'feature', key: 'feature1', user: user, variation: 0, value: 'd', default: nil, version: 2, prereqOf: 'feature0' }] - result = evaluate(flag, user, features, logger) + result = evaluate(flag, user, features, logger, factory) expect(result.detail).to eq(detail) expect(result.events).to eq(events_should_be) end @@ -218,10 +217,9 @@ def boolean_flag_with_clauses(clauses) user = { key: 'x' } detail = LaunchDarkly::EvaluationDetail.new('a', 0, { kind: 'FALLTHROUGH' }) events_should_be = [{ - kind: 'feature', key: 'feature1', user: user, variation: 1, value: 'e', version: 2, prereqOf: 'feature0', - trackEvents: nil, debugEventsUntilDate: nil + kind: 'feature', key: 'feature1', user: user, variation: 1, value: 'e', default: nil, version: 2, prereqOf: 'feature0' }] - result = evaluate(flag, user, features, logger) + result = evaluate(flag, user, features, logger, factory) expect(result.detail).to eq(detail) expect(result.events).to eq(events_should_be) end @@ -236,7 +234,7 @@ def boolean_flag_with_clauses(clauses) } user = { key: 'userkey' } detail = LaunchDarkly::EvaluationDetail.new(nil, nil, { kind: 'ERROR', errorKind: 'MALFORMED_FLAG' }) - result = evaluate(flag, user, features, logger) + result = evaluate(flag, user, features, logger, factory) expect(result.detail).to eq(detail) expect(result.events).to eq([]) end @@ -251,7 +249,7 @@ def boolean_flag_with_clauses(clauses) } user = { key: 'userkey' } detail = LaunchDarkly::EvaluationDetail.new(nil, nil, { kind: 'ERROR', errorKind: 'MALFORMED_FLAG' }) - result = evaluate(flag, user, features, logger) + result = evaluate(flag, user, features, logger, factory) expect(result.detail).to eq(detail) expect(result.events).to eq([]) end @@ -266,7 +264,7 @@ def boolean_flag_with_clauses(clauses) } user = { key: 'userkey' } detail = LaunchDarkly::EvaluationDetail.new(nil, nil, { kind: 'ERROR', errorKind: 'MALFORMED_FLAG' }) - result = evaluate(flag, user, features, logger) + result = evaluate(flag, user, features, logger, factory) expect(result.detail).to eq(detail) expect(result.events).to eq([]) end @@ -281,7 +279,7 @@ def boolean_flag_with_clauses(clauses) } user = { key: 'userkey' } detail = LaunchDarkly::EvaluationDetail.new(nil, nil, { kind: 'ERROR', errorKind: 'MALFORMED_FLAG' }) - result = evaluate(flag, user, features, logger) + result = evaluate(flag, user, features, logger, factory) expect(result.detail).to eq(detail) expect(result.events).to eq([]) end @@ -299,7 +297,7 @@ def boolean_flag_with_clauses(clauses) } user = { key: 'userkey' } detail = LaunchDarkly::EvaluationDetail.new('c', 2, { kind: 'TARGET_MATCH' }) - result = evaluate(flag, user, features, logger) + result = evaluate(flag, user, features, logger, factory) expect(result.detail).to eq(detail) expect(result.events).to eq([]) end @@ -310,7 +308,7 @@ def boolean_flag_with_clauses(clauses) user = { key: 'userkey' } detail = LaunchDarkly::EvaluationDetail.new(true, 1, { kind: 'RULE_MATCH', ruleIndex: 0, ruleId: 'ruleid' }) - result = evaluate(flag, user, features, logger) + result = evaluate(flag, user, features, logger, factory) expect(result.detail).to eq(detail) expect(result.events).to eq([]) end @@ -321,7 +319,7 @@ def boolean_flag_with_clauses(clauses) user = { key: 'userkey' } detail = LaunchDarkly::EvaluationDetail.new(nil, nil, { kind: 'ERROR', errorKind: 'MALFORMED_FLAG' }) - result = evaluate(flag, user, features, logger) + result = evaluate(flag, user, features, logger, factory) expect(result.detail).to eq(detail) expect(result.events).to eq([]) end @@ -332,7 +330,7 @@ def boolean_flag_with_clauses(clauses) user = { key: 'userkey' } detail = LaunchDarkly::EvaluationDetail.new(nil, nil, { kind: 'ERROR', errorKind: 'MALFORMED_FLAG' }) - result = evaluate(flag, user, features, logger) + result = evaluate(flag, user, features, logger, factory) expect(result.detail).to eq(detail) expect(result.events).to eq([]) end @@ -343,7 +341,7 @@ def boolean_flag_with_clauses(clauses) user = { key: 'userkey' } detail = LaunchDarkly::EvaluationDetail.new(nil, nil, { kind: 'ERROR', errorKind: 'MALFORMED_FLAG' }) - result = evaluate(flag, user, features, logger) + result = evaluate(flag, user, features, logger, factory) expect(result.detail).to eq(detail) expect(result.events).to eq([]) end @@ -355,7 +353,7 @@ def boolean_flag_with_clauses(clauses) user = { key: 'userkey' } detail = LaunchDarkly::EvaluationDetail.new(nil, nil, { kind: 'ERROR', errorKind: 'MALFORMED_FLAG' }) - result = evaluate(flag, user, features, logger) + result = evaluate(flag, user, features, logger, factory) expect(result.detail).to eq(detail) expect(result.events).to eq([]) end @@ -364,7 +362,7 @@ def boolean_flag_with_clauses(clauses) clause = { attribute: 'key', op: 'in', values: ['999'] } flag = boolean_flag_with_clauses([clause]) user = { key: 999 } - result = evaluate(flag, user, features, logger) + result = evaluate(flag, user, features, logger, factory) expect(result.detail.value).to eq(true) end @@ -375,7 +373,7 @@ def boolean_flag_with_clauses(clauses) rollout: { salt: '', variations: [ { weight: 100000, variation: 1 } ] } } flag = boolean_flag_with_rules([rule]) user = { key: "userkey", secondary: 999 } - result = evaluate(flag, user, features, logger) + result = evaluate(flag, user, features, logger, factory) expect(result.detail.reason).to eq({ kind: 'RULE_MATCH', ruleIndex: 0, ruleId: 'ruleid'}) end end @@ -385,28 +383,28 @@ def boolean_flag_with_clauses(clauses) user = { key: 'x', name: 'Bob' } clause = { attribute: 'name', op: 'in', values: ['Bob'] } flag = boolean_flag_with_clauses([clause]) - expect(evaluate(flag, user, features, logger).detail.value).to be true + expect(evaluate(flag, user, features, logger, factory).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(evaluate(flag, user, features, logger).detail.value).to be true + expect(evaluate(flag, user, features, logger, factory).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(evaluate(flag, user, features, logger).detail.value).to be false + expect(evaluate(flag, user, features, logger, factory).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(evaluate(flag, user, features, logger).detail.value).to be false + expect(evaluate(flag, user, features, logger, factory).detail.value).to be false end it "does not stop evaluating rules after clause with unknown operator" do @@ -416,14 +414,14 @@ def boolean_flag_with_clauses(clauses) clause1 = { attribute: 'name', op: 'in', values: ['Bob'] } rule1 = { clauses: [ clause1 ], variation: 1 } flag = boolean_flag_with_rules([rule0, rule1]) - expect(evaluate(flag, user, features, logger).detail.value).to be true + expect(evaluate(flag, user, features, logger, factory).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(evaluate(flag, user, features, logger).detail.value).to be false + expect(evaluate(flag, user, features, logger, factory).detail.value).to be false end it "retrieves segment from segment store for segmentMatch operator" do @@ -438,14 +436,14 @@ def boolean_flag_with_clauses(clauses) user = { key: 'userkey' } clause = { attribute: '', op: 'segmentMatch', values: ['segkey'] } flag = boolean_flag_with_clauses([clause]) - expect(evaluate(flag, user, features, logger).detail.value).to be true + expect(evaluate(flag, user, features, logger, factory).detail.value).to be true end it "falls through with no errors if referenced segment is not found" do user = { key: 'userkey' } clause = { attribute: '', op: 'segmentMatch', values: ['segkey'] } flag = boolean_flag_with_clauses([clause]) - expect(evaluate(flag, user, features, logger).detail.value).to be false + expect(evaluate(flag, user, features, logger, factory).detail.value).to be false end it "can be negated" do @@ -454,7 +452,7 @@ def boolean_flag_with_clauses(clauses) flag = boolean_flag_with_clauses([clause]) expect { clause[:negate] = true - }.to change {evaluate(flag, user, features, logger).detail.value}.from(true).to(false) + }.to change {evaluate(flag, user, features, logger, factory).detail.value}.from(true).to(false) end end @@ -557,7 +555,7 @@ def boolean_flag_with_clauses(clauses) user = { key: 'x', custom: { foo: value1 } } clause = { attribute: 'foo', op: op, values: [value2] } flag = boolean_flag_with_clauses([clause]) - expect(evaluate(flag, user, features, logger).detail.value).to be shouldBe + expect(evaluate(flag, user, features, logger, factory).detail.value).to be shouldBe end end end @@ -648,7 +646,7 @@ def test_segment_match(segment) features.upsert(LaunchDarkly::SEGMENTS, segment) clause = make_segment_match_clause(segment) flag = boolean_flag_with_clauses([clause]) - evaluate(flag, user, features, logger).detail.value + evaluate(flag, user, features, logger, factory).detail.value end it 'explicitly includes user' do diff --git a/spec/events_spec.rb b/spec/events_spec.rb index 557c3594..16bee286 100644 --- a/spec/events_spec.rb +++ b/spec/events_spec.rb @@ -342,7 +342,7 @@ it "queues custom event with user" do @ep = subject.new("sdk_key", default_config, hc) - e = { kind: "custom", key: "eventkey", user: user, data: { thing: "stuff" } } + e = { kind: "custom", key: "eventkey", user: user, data: { thing: "stuff" }, metricValue: 1.5 } @ep.add_event(e) output = flush_and_get_events @@ -565,6 +565,7 @@ def custom_event(e, inline_user) else out[:user] = inline_user end + out[:metricValue] = e[:metricValue] if e.has_key?(:metricValue) out end diff --git a/spec/ldclient_spec.rb b/spec/ldclient_spec.rb index 86cb5be5..4672a662 100644 --- a/spec/ldclient_spec.rb +++ b/spec/ldclient_spec.rb @@ -25,6 +25,22 @@ } } end + let(:numeric_key_user) do + { + key: 33, + custom: { + groups: [ "microsoft", "google" ] + } + } + end + let(:sanitized_numeric_key_user) do + { + key: "33", + custom: { + groups: [ "microsoft", "google" ] + } + } + end let(:user_without_key) do { name: "Keyless Joe" } end @@ -91,7 +107,6 @@ def event_processor key: "key", version: 100, user: nil, - variation: nil, value: "default", default: "default", trackEvents: true, @@ -109,7 +124,6 @@ def event_processor key: "key", version: 100, user: bad_user, - variation: nil, value: "default", default: "default", trackEvents: true, @@ -117,6 +131,61 @@ def event_processor )) client.variation("key", bad_user, "default") end + + it "sets trackEvents and reason if trackEvents is set for matched rule" do + flag = { + key: 'flag', + on: true, + variations: [ 'value' ], + version: 100, + rules: [ + clauses: [ + { attribute: 'key', op: 'in', values: [ user[:key] ] } + ], + variation: 0, + id: 'id', + trackEvents: true + ] + } + config.feature_store.init({ LaunchDarkly::FEATURES => {} }) + config.feature_store.upsert(LaunchDarkly::FEATURES, flag) + expect(event_processor).to receive(:add_event).with(hash_including( + kind: 'feature', + key: 'flag', + version: 100, + user: user, + value: 'value', + default: 'default', + trackEvents: true, + reason: { kind: 'RULE_MATCH', ruleIndex: 0, ruleId: 'id' } + )) + client.variation('flag', user, 'default') + end + + it "sets trackEvents and reason if trackEventsFallthrough is set and we fell through" do + flag = { + key: 'flag', + on: true, + variations: [ 'value' ], + fallthrough: { variation: 0 }, + version: 100, + rules: [], + trackEventsFallthrough: true + } + config.feature_store.init({ LaunchDarkly::FEATURES => {} }) + config.feature_store.upsert(LaunchDarkly::FEATURES, flag) + expect(event_processor).to receive(:add_event).with(hash_including( + kind: 'feature', + key: 'flag', + version: 100, + user: user, + value: 'value', + default: 'default', + trackEvents: true, + reason: { kind: 'FALLTHROUGH' } + )) + client.variation('flag', user, 'default') + end end describe '#variation_detail' do @@ -338,6 +407,17 @@ def event_processor client.track("custom_event_name", user, 42) end + it "can include a metric value" do + expect(event_processor).to receive(:add_event).with(hash_including( + kind: "custom", key: "custom_event_name", user: user, metricValue: 1.5)) + client.track("custom_event_name", user, nil, 1.5) + end + + it "sanitizes the user in the event" do + expect(event_processor).to receive(:add_event).with(hash_including(user: sanitized_numeric_key_user)) + client.track("custom_event_name", numeric_key_user, nil) + end + it "does not send an event, and logs a warning, if user is nil" do expect(event_processor).not_to receive(:add_event) expect(logger).to receive(:warn)