lib/ldclient-rb/ldclient.rb in launchdarkly-server-sdk-6.4.0 vs lib/ldclient-rb/ldclient.rb in launchdarkly-server-sdk-7.0.0

- old
+ new

@@ -57,11 +57,11 @@ @big_segment_store_manager = Impl::BigSegmentStoreManager.new(config.big_segments, @config.logger) @big_segment_store_status_provider = @big_segment_store_manager.status_provider get_flag = lambda { |key| @store.get(FEATURES, key) } get_segment = lambda { |key| @store.get(SEGMENTS, key) } - get_big_segments_membership = lambda { |key| @big_segment_store_manager.get_user_membership(key) } + get_big_segments_membership = lambda { |key| @big_segment_store_manager.get_context_membership(key) } @evaluator = LaunchDarkly::Impl::Evaluator.new(get_flag, get_segment, get_big_segments_membership, @config.logger) if !@config.offline? && @config.send_events && !@config.diagnostic_opt_out? diagnostic_accumulator = Impl::DiagnosticAccumulator.new(Impl::DiagnosticAccumulator.create_diagnostic_id(sdk_key)) else @@ -118,30 +118,24 @@ def flush @event_processor.flush end # - # @param key [String] the feature flag key - # @param user [Hash] the user properties - # @param default [Boolean] (false) the value to use if the flag cannot be evaluated - # @return [Boolean] the flag value - # @deprecated Use {#variation} instead. - # - def toggle?(key, user, default = false) - @config.logger.warn { "[LDClient] toggle? is deprecated. Use variation instead" } - variation(key, user, default) - end - - # - # Creates a hash string that can be used by the JavaScript SDK to identify a user. + # Creates a hash string that can be used by the JavaScript SDK to identify a context. # For more information, see [Secure mode](https://docs.launchdarkly.com/sdk/features/secure-mode#ruby). # - # @param user [Hash] the user properties - # @return [String] a hash string + # @param context [Hash, LDContext] + # @return [String, nil] a hash string or nil if the provided context was invalid # - def secure_mode_hash(user) - OpenSSL::HMAC.hexdigest("sha256", @sdk_key, user[:key].to_s) + def secure_mode_hash(context) + context = Impl::Context::make_context(context) + unless context.valid? + @config.logger.warn("secure_mode_hash called with invalid context: #{context.error}") + return nil + end + + OpenSSL::HMAC.hexdigest("sha256", @sdk_key, context.fully_qualified_key) end # # Returns whether the client has been initialized and is ready to serve feature flag requests. # @@ -163,48 +157,26 @@ def initialized? @config.offline? || @config.use_ldd? || @data_source.initialized? end # - # Determines the variation of a feature flag to present to a user. + # Determines the variation of a feature flag to present for a context. # - # At a minimum, the user hash should contain a `:key`, which should be the unique - # identifier for your user (or, for an anonymous user, a session identifier or - # cookie). - # - # Other supported user attributes include IP address, country code, and an arbitrary hash of - # custom attributes. For more about the supported user properties and how they work in - # LaunchDarkly, see [Targeting users](https://docs.launchdarkly.com/home/flags/targeting-users). - # - # The optional `:privateAttributeNames` user property allows you to specify a list of - # attribute names that should not be sent back to LaunchDarkly. - # [Private attributes](https://docs.launchdarkly.com/home/users/attributes#creating-private-user-attributes) - # can also be configured globally in {Config}. - # - # @example Basic user hash - # {key: "my-user-id"} - # - # @example More complete user hash - # {key: "my-user-id", ip: "127.0.0.1", country: "US", custom: {customer_rank: 1000}} - # - # @example User with a private attribute - # {key: "my-user-id", email: "email@example.com", privateAttributeNames: ["email"]} - # # @param key [String] the unique feature key for the feature flag, as shown # on the LaunchDarkly dashboard - # @param user [Hash] a hash containing parameters for the end user requesting the flag + # @param context [Hash, LDContext] a hash or LDContext instance describing the context requesting the flag # @param default the default value of the flag; this is used if there is an error # condition making it impossible to find or evaluate the flag # - # @return the variation to show the user, or the default value if there's an an error + # @return the variation for the provided context, or the default value if there's an an error # - def variation(key, user, default) - evaluate_internal(key, user, default, false).value + def variation(key, context, default) + evaluate_internal(key, context, default, false).value end # - # Determines the variation of a feature flag for a user, like {#variation}, but also + # Determines the variation of a feature flag for a context, like {#variation}, but also # provides additional information about how this value was calculated. # # The return value of `variation_detail` is an {EvaluationDetail} object, which has # three properties: the result value, the positional index of this value in the flag's # list of variations, and an object describing the main reason why this value was @@ -216,112 +188,86 @@ # For more information, see the reference guide on # [Evaluation reasons](https://docs.launchdarkly.com/sdk/concepts/evaluation-reasons). # # @param key [String] the unique feature key for the feature flag, as shown # on the LaunchDarkly dashboard - # @param user [Hash] a hash containing parameters for the end user requesting the flag + # @param context [Hash, LDContext] a hash or object describing the context requesting the flag, # @param default the default value of the flag; this is used if there is an error # condition making it impossible to find or evaluate the flag # # @return [EvaluationDetail] an object describing the result # - def variation_detail(key, user, default) - evaluate_internal(key, user, default, true) + def variation_detail(key, context, default) + evaluate_internal(key, context, default, true) end # - # Registers the user. This method simply creates an analytics event containing the user - # properties, so that LaunchDarkly will know about that user if it does not already. + # Registers the context. This method simply creates an analytics event containing the context + # properties, so that LaunchDarkly will know about that context if it does not already. # - # Calling {#variation} or {#variation_detail} also sends the user information to + # Calling {#variation} or {#variation_detail} also sends the context information to # LaunchDarkly (if events are enabled), so you only need to use {#identify} if you - # want to identify the user without evaluating a flag. + # want to identify the context without evaluating a flag. # # Note that event delivery is asynchronous, so the event may not actually be sent # until later; see {#flush}. # - # @param user [Hash] The user to register; this can have all the same user properties - # described in {#variation} + # @param context [Hash, LDContext] a hash or object describing the context to register # @return [void] # - def identify(user) - if !user || user[:key].nil? || user[:key].empty? - @config.logger.warn("Identify called with nil user or empty user key!") + def identify(context) + context = LaunchDarkly::Impl::Context.make_context(context) + unless context.valid? + @config.logger.warn("Identify called with invalid context: #{context.error}") return end - sanitize_user(user) - @event_processor.record_identify_event(user) + + if context.key == "" + @config.logger.warn("Identify called with empty key") + return + end + + @event_processor.record_identify_event(context) end # - # Tracks that a user performed an event. This method creates a "custom" analytics event - # containing the specified event name (key), user properties, and optional data. + # Tracks that a context performed an event. This method creates a "custom" analytics event + # containing the specified event name (key), context properties, and optional data. # # 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/sdk/features/events#ruby) # 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 context [Hash, LDContext] a hash or object describing the context to track # @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 = nil, metric_value = nil) - if !user || user[:key].nil? - @config.logger.warn("Track called with nil user or nil user key!") + def track(event_name, context, data = nil, metric_value = nil) + context = LaunchDarkly::Impl::Context.make_context(context) + unless context.valid? + @config.logger.warn("Track called with invalid context: #{context.error}") return end - sanitize_user(user) - @event_processor.record_custom_event(user, event_name, data, metric_value) - end - # - # Associates a new and old user object for analytics purposes via an alias event. - # - # @param current_context [Hash] The current version of a user. - # @param previous_context [Hash] The previous version of a user. - # @return [void] - # - def alias(current_context, previous_context) - if !current_context || current_context[:key].nil? || !previous_context || previous_context[:key].nil? - @config.logger.warn("Alias called with nil user or nil user key!") - return - end - sanitize_user(current_context) - sanitize_user(previous_context) - @event_processor.record_alias_event(current_context, previous_context) + @event_processor.record_custom_event(context, event_name, data, metric_value) end # - # Returns all feature flag values for the given user. - # - # @deprecated Please use {#all_flags_state} instead. Current versions of the - # client-side SDK will not generate analytics events correctly if you pass the - # result of `all_flags`. - # - # @param user [Hash] The end user requesting the feature flags - # @return [Hash] a hash of feature flag keys to values - # - def all_flags(user) - all_flags_state(user).values_map - end - - # - # Returns a {FeatureFlagsState} object that encapsulates the state of all feature flags for a given user, + # Returns a {FeatureFlagsState} object that encapsulates the state of all feature flags for a given context, # including the flag values and also metadata that can be used on the front end. This method does not # send analytics events back to LaunchDarkly. # - # @param user [Hash] The end user requesting the feature flags + # @param context [Hash, LDContext] a hash or object describing the context requesting the flags, # @param options [Hash] Optional parameters to control how the state is generated # @option options [Boolean] :client_side_only (false) True if only flags marked for use with the # client-side SDK should be included in the state. By default, all flags are included. # @option options [Boolean] :with_reasons (false) True if evaluation reasons should be included # in the state (see {#variation_detail}). By default, they are not included. @@ -329,24 +275,25 @@ # normally only used for event generation - such as flag versions and evaluation reasons - should be # omitted for any flag that does not have event tracking or debugging turned on. This reduces the size # of the JSON data if you are passing the flag state to the front end. # @return [FeatureFlagsState] a {FeatureFlagsState} object which can be serialized to JSON # - def all_flags_state(user, options={}) + def all_flags_state(context, options={}) return FeatureFlagsState.new(false) if @config.offline? - if !initialized? + unless initialized? if @store.initialized? @config.logger.warn { "Called all_flags_state before client initialization; using last known values from data store" } else @config.logger.warn { "Called all_flags_state before client initialization. Data store not available; returning empty state" } return FeatureFlagsState.new(false) end end - unless user && !user[:key].nil? - @config.logger.error { "[LDClient] User and user key must be specified in all_flags_state" } + context = Impl::Context::make_context(context) + unless context.valid? + @config.logger.error { "[LDClient] Context was invalid for all_flags_state (#{context.error})" } return FeatureFlagsState.new(false) end begin features = @store.all(FEATURES) @@ -362,17 +309,17 @@ features.each do |k, f| if client_only && !f[:clientSide] next end begin - detail = @evaluator.evaluate(f, user).detail + detail = @evaluator.evaluate(f, context).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 = is_experiment(f, detail.reason) + requires_experiment_data = experiment?(f, detail.reason) flag_state = { key: f[:key], value: detail.value, variation: detail.variation_index, reason: detail.reason, @@ -423,73 +370,75 @@ requestor = Requestor.new(sdk_key, config) PollingProcessor.new(config, requestor) end end + # @param context [Hash, LDContext] # @return [EvaluationDetail] - def evaluate_internal(key, user, default, with_reasons) + def evaluate_internal(key, context, default, with_reasons) if @config.offline? return Evaluator.error_result(EvaluationReason::ERROR_CLIENT_NOT_READY, default) end - unless user - @config.logger.error { "[LDClient] Must specify user" } + if context.nil? + @config.logger.error { "[LDClient] Must specify context" } detail = Evaluator.error_result(EvaluationReason::ERROR_USER_NOT_SPECIFIED, default) return detail end - if user[:key].nil? - @config.logger.warn { "[LDClient] Variation called with nil user key; returning default value" } + context = Impl::Context::make_context(context) + unless context.valid? + @config.logger.error { "[LDClient] Context was invalid for flag evaluation (#{context.error}); returning default value" } detail = Evaluator.error_result(EvaluationReason::ERROR_USER_NOT_SPECIFIED, default) return detail end - if !initialized? + unless initialized? if @store.initialized? @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" } detail = Evaluator.error_result(EvaluationReason::ERROR_CLIENT_NOT_READY, default) - record_unknown_flag_eval(key, user, default, detail.reason, with_reasons) - return detail + record_unknown_flag_eval(key, context, default, detail.reason, with_reasons) + return detail end end feature = @store.get(FEATURES, key) if feature.nil? @config.logger.info { "[LDClient] Unknown feature flag \"#{key}\". Returning default value" } detail = Evaluator.error_result(EvaluationReason::ERROR_FLAG_NOT_FOUND, default) - record_unknown_flag_eval(key, user, default, detail.reason, with_reasons) + record_unknown_flag_eval(key, context, default, detail.reason, with_reasons) return detail end begin - res = @evaluator.evaluate(feature, user) - if !res.prereq_evals.nil? + res = @evaluator.evaluate(feature, context) + unless 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) + record_prereq_flag_eval(prereq_eval.prereq_flag, prereq_eval.prereq_of_flag, context, prereq_eval.detail, with_reasons) end end detail = res.detail if detail.default_value? detail = EvaluationDetail.new(default, nil, detail.reason) end - record_flag_eval(feature, user, detail, default, with_reasons) - return detail + record_flag_eval(feature, context, detail, default, with_reasons) + detail rescue => exn Util.log_exception(@config.logger, "Error evaluating feature flag \"#{key}\"", exn) detail = Evaluator.error_result(EvaluationReason::ERROR_EXCEPTION, default) - record_flag_eval_error(feature, user, default, detail.reason, with_reasons) - return detail + record_flag_eval_error(feature, context, default, detail.reason, with_reasons) + detail end end - private def record_flag_eval(flag, user, detail, default, with_reasons) - add_experiment_data = is_experiment(flag, detail.reason) + private def record_flag_eval(flag, context, detail, default, with_reasons) + add_experiment_data = experiment?(flag, detail.reason) @event_processor.record_eval_event( - user, + context, flag[:key], flag[:version], detail.variation_index, detail.value, (add_experiment_data || with_reasons) ? detail.reason : nil, @@ -497,15 +446,15 @@ 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) + + private def record_prereq_flag_eval(prereq_flag, prereq_of_flag, context, detail, with_reasons) + add_experiment_data = experiment?(prereq_flag, detail.reason) @event_processor.record_eval_event( - user, + context, prereq_flag[:key], prereq_flag[:version], detail.variation_index, detail.value, (add_experiment_data || with_reasons) ? detail.reason : nil, @@ -513,44 +462,45 @@ 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, + + private def record_flag_eval_error(flag, context, default, reason, with_reasons) + @event_processor.record_eval_event(context, 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, + # + # @param flag_key [String] + # @param context [LaunchDarkly::LDContext] + # @param default [any] + # @param reason [LaunchDarkly::EvaluationReason] + # @param with_reasons [Boolean] + # + private def record_unknown_flag_eval(flag_key, context, default, reason, with_reasons) + @event_processor.record_eval_event(context, flag_key, nil, nil, default, with_reasons ? reason : nil, default, false, nil, nil) end - private def is_experiment(flag, reason) - return false if !reason + private def experiment?(flag, reason) + return false unless reason if reason.in_experiment return true end case reason[:kind] when 'RULE_MATCH' index = reason[:ruleIndex] - if !index.nil? + unless 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 end end # # Used internally when the client is offline.