diff --git a/lib/optimizely.rb b/lib/optimizely.rb index 22d44548..ae5e9200 100644 --- a/lib/optimizely.rb +++ b/lib/optimizely.rb @@ -38,6 +38,9 @@ require_relative 'optimizely/notification_center' require_relative 'optimizely/optimizely_config' require_relative 'optimizely/optimizely_user_context' +require_relative 'optimizely/odp/lru_cache' +require_relative 'optimizely/odp/odp_manager' +require_relative 'optimizely/helpers/sdk_settings' module Optimizely class Project @@ -46,7 +49,7 @@ class Project attr_reader :notification_center # @api no-doc attr_reader :config_manager, :decision_service, :error_handler, :event_dispatcher, - :event_processor, :logger, :stopped + :event_processor, :logger, :odp_manager, :stopped # Constructor for Projects. # @@ -62,6 +65,8 @@ class Project # @param config_manager - Optional Responds to 'config' method. # @param notification_center - Optional Instance of NotificationCenter. # @param event_processor - Optional Responds to process. + # @param default_decide_options: Optional default decision options. + # @param settings: Optional instance of OptimizelySdkSettings for sdk configuration. def initialize( # rubocop:disable Metrics/ParameterLists datafile = nil, @@ -74,13 +79,15 @@ def initialize( # rubocop:disable Metrics/ParameterLists config_manager = nil, notification_center = nil, event_processor = nil, - default_decide_options = [] + default_decide_options = [], + settings = nil ) @logger = logger || NoOpLogger.new @error_handler = error_handler || NoOpErrorHandler.new @event_dispatcher = event_dispatcher || EventDispatcher.new(logger: @logger, error_handler: @error_handler) @user_profile_service = user_profile_service @default_decide_options = [] + @sdk_settings = settings if default_decide_options.is_a? Array @default_decide_options = default_decide_options.clone @@ -98,6 +105,16 @@ def initialize( # rubocop:disable Metrics/ParameterLists @notification_center = notification_center.is_a?(Optimizely::NotificationCenter) ? notification_center : NotificationCenter.new(@logger, @error_handler) + setup_odp! + + @odp_manager = OdpManager.new( + disable: @sdk_settings.odp_disabled, + segment_manager: @sdk_settings.odp_segment_manager, + event_manager: @sdk_settings.odp_event_manager, + segments_cache: @sdk_settings.odp_segments_cache, + logger: @logger + ) + @config_manager = if config_manager.respond_to?(:config) config_manager elsif sdk_key @@ -113,6 +130,10 @@ def initialize( # rubocop:disable Metrics/ParameterLists StaticProjectConfigManager.new(datafile, @logger, @error_handler, skip_json_validation) end + # must call this even if it's scheduled as a listener + # in case the config manager was initialized before the listener was added + update_odp_config_on_datafile_update unless @sdk_settings.odp_disabled + @decision_service = DecisionService.new(@logger, @user_profile_service) @event_processor = if event_processor.respond_to?(:process) @@ -816,6 +837,7 @@ def close @stopped = true @config_manager.stop! if @config_manager.respond_to?(:stop!) @event_processor.stop! if @event_processor.respond_to?(:stop!) + @odp_manager.stop! end def get_optimizely_config @@ -869,6 +891,25 @@ def get_optimizely_config end end + # Send an event to the ODP server. + # + # @param action - the event action name. + # @param type - the event type (default = "fullstack"). + # @param identifiers - a hash for identifiers. + # @param data - a hash for associated data. The default event data will be added to this data before sending to the ODP server. + + def send_odp_event(action:, type: Helpers::Constants::ODP_MANAGER_CONFIG[:EVENT_TYPE], identifiers: {}, data: {}) + @odp_manager.send_event(type: type, action: action, identifiers: identifiers, data: data) + end + + def identify_user(user_id:) + @odp_manager.identify_user(user_id: user_id) + end + + def fetch_qualified_segments(user_id:, options: []) + @odp_manager.fetch_qualified_segments(user_id: user_id, options: options) + end + private def get_variation_with_config(experiment_key, user_id, attributes, config) @@ -1126,5 +1167,51 @@ def send_impression(config, experiment, variation_key, flag_key, rule_key, enabl def project_config @config_manager.config end + + def update_odp_config_on_datafile_update + # if datafile isn't ready, expects to be called again by the notification_center + return if @config_manager.respond_to?(:ready?) && !@config_manager.ready? + + config = @config_manager&.config + return unless config + + @odp_manager.update_odp_config(config.public_key_for_odp, config.host_for_odp, config.all_segments) + end + + def setup_odp! + unless @sdk_settings.is_a? Optimizely::Helpers::OptimizelySdkSettings + @logger.log(Logger::DEBUG, 'Provided sdk_settings is not an OptimizelySdkSettings instance.') unless @sdk_settings.nil? + @sdk_settings = Optimizely::Helpers::OptimizelySdkSettings.new + end + + return if @sdk_settings.odp_disabled + + @notification_center.add_notification_listener( + NotificationCenter::NOTIFICATION_TYPES[:OPTIMIZELY_CONFIG_UPDATE], + -> { update_odp_config_on_datafile_update } + ) + + if !@sdk_settings.odp_segment_manager.nil? && !Helpers::Validator.segment_manager_valid?(@sdk_settings.odp_segment_manager) + @logger.log(Logger::ERROR, 'Invalid ODP segment manager, reverting to default.') + @sdk_settings.odp_segment_manager = nil + end + + if !@sdk_settings.odp_event_manager.nil? && !Helpers::Validator.event_manager_valid?(@sdk_settings.odp_event_manager) + @logger.log(Logger::ERROR, 'Invalid ODP event manager, reverting to default.') + @sdk_settings.odp_event_manager = nil + end + + return if @sdk_settings.odp_segment_manager + + if !@sdk_settings.odp_segments_cache.nil? && !Helpers::Validator.segments_cache_valid?(@sdk_settings.odp_segments_cache) + @logger.log(Logger::ERROR, 'Invalid ODP segments cache, reverting to default.') + @sdk_settings.odp_segments_cache = nil + end + + @sdk_settings.odp_segments_cache ||= LRUCache.new( + @sdk_settings.segments_cache_size, + @sdk_settings.segments_cache_timeout_in_secs + ) + end end end diff --git a/lib/optimizely/helpers/sdk_settings.rb b/lib/optimizely/helpers/sdk_settings.rb new file mode 100644 index 00000000..335d5f4b --- /dev/null +++ b/lib/optimizely/helpers/sdk_settings.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +# +# Copyright 2022, Optimizely and contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require_relative 'constants' + +module Optimizely + module Helpers + class OptimizelySdkSettings + attr_accessor :odp_disabled, :segments_cache_size, :segments_cache_timeout_in_secs, :odp_segments_cache, :odp_segment_manager, :odp_event_manager + + # Contains configuration used for Optimizely Project initialization. + # + # @param disable_odp - Set this flag to true (default = false) to disable ODP features. + # @param segments_cache_size - The maximum size of audience segments cache (optional. default = 10,000). Set to zero to disable caching. + # @param segments_cache_timeout_in_secs - The timeout in seconds of audience segments cache (optional. default = 600). Set to zero to disable timeout. + # @param odp_segments_cache - A custom odp segments cache. Required methods include: `save(key, value)`, `lookup(key) -> value`, and `reset()` + # @param odp_segment_manager - A custom odp segment manager. Required method is: `fetch_qualified_segments(user_key, user_value, options)`. + # @param odp_event_manager - A custom odp event manager. Required method is: `send_event(type:, action:, identifiers:, data:)` + def initialize( + disable_odp: false, + segments_cache_size: Constants::ODP_SEGMENTS_CACHE_CONFIG[:DEFAULT_CAPACITY], + segments_cache_timeout_in_secs: Constants::ODP_SEGMENTS_CACHE_CONFIG[:DEFAULT_TIMEOUT_SECONDS], + odp_segments_cache: nil, + odp_segment_manager: nil, + odp_event_manager: nil + ) + @odp_disabled = disable_odp + @segments_cache_size = segments_cache_size + @segments_cache_timeout_in_secs = segments_cache_timeout_in_secs + @odp_segments_cache = odp_segments_cache + @odp_segment_manager = odp_segment_manager + @odp_event_manager = odp_event_manager + end + end + end +end diff --git a/lib/optimizely/helpers/validator.rb b/lib/optimizely/helpers/validator.rb index 4d38b24c..3d7631a5 100644 --- a/lib/optimizely/helpers/validator.rb +++ b/lib/optimizely/helpers/validator.rb @@ -183,6 +183,56 @@ def odp_data_types_valid?(data) valid_types = [String, Float, Integer, TrueClass, FalseClass, NilClass] data.values.all? { |e| valid_types.member? e.class } end + + def segments_cache_valid?(segments_cache) + # Determines if a given segments_cache is valid. + # + # segments_cache - custom cache to be validated. + # + # Returns boolean depending on whether cache has required methods. + ( + segments_cache.respond_to?(:reset) && + segments_cache.method(:reset)&.parameters&.empty? && + segments_cache.respond_to?(:lookup) && + segments_cache.method(:lookup)&.parameters&.length&.positive? && + segments_cache.respond_to?(:save) && + segments_cache.method(:save)&.parameters&.length&.positive? + ) + end + + def segment_manager_valid?(segment_manager) + # Determines if a given segment_manager is valid. + # + # segment_manager - custom manager to be validated. + # + # Returns boolean depending on whether manager has required methods. + ( + segment_manager.respond_to?(:odp_config) && + segment_manager.respond_to?(:reset) && + segment_manager.method(:reset)&.parameters&.empty? && + segment_manager.respond_to?(:fetch_qualified_segments) && + (segment_manager.method(:fetch_qualified_segments)&.parameters&.length || 0) >= 3 + ) + end + + def event_manager_valid?(event_manager) + # Determines if a given event_manager is valid. + # + # event_manager - custom manager to be validated. + # + # Returns boolean depending on whether manager has required method and parameters. + return false unless + event_manager.respond_to?(:send_event) && + event_manager.respond_to?(:start!) && + (event_manager.method(:start!)&.parameters&.length || 0) >= 1 && + event_manager.respond_to?(:update_config) && + event_manager.respond_to?(:stop!) + + required_parameters = Set[%i[keyreq type], %i[keyreq action], %i[keyreq identifiers], %i[keyreq data]] + existing_parameters = event_manager.method(:send_event).parameters.to_set + + existing_parameters & required_parameters == required_parameters + end end end end diff --git a/lib/optimizely/odp/odp_config.rb b/lib/optimizely/odp/odp_config.rb index 655274e7..e425e59f 100644 --- a/lib/optimizely/odp/odp_config.rb +++ b/lib/optimizely/odp/odp_config.rb @@ -67,14 +67,6 @@ def api_host @mutex.synchronize { @api_host.clone } end - # Returns the api host for odp connections - # - # @return - The api host. - - def api_host=(api_host) - @mutex.synchronize { @api_host = api_host.clone } - end - # Returns the api key for odp connections # # @return - The api key. @@ -83,14 +75,6 @@ def api_key @mutex.synchronize { @api_key.clone } end - # Replace the api key with the provided string - # - # @param api_key - An api key - - def api_key=(api_key) - @mutex.synchronize { @api_key = api_key.clone } - end - # Returns An array of qualified segments for this user # # @return - An array of segments names. diff --git a/lib/optimizely/odp/odp_event_manager.rb b/lib/optimizely/odp/odp_event_manager.rb index d23951ee..b18a3a28 100644 --- a/lib/optimizely/odp/odp_event_manager.rb +++ b/lib/optimizely/odp/odp_event_manager.rb @@ -15,8 +15,9 @@ # See the License for the specific language governing permissions and # limitations under the License. # -require_relative 'zaius_rest_api_manager' +require_relative 'odp_events_api_manager' require_relative '../helpers/constants' +require_relative 'odp_event' module Optimizely class OdpEventManager @@ -26,7 +27,7 @@ class OdpEventManager # the BlockingQueue and buffers them for either a configured batch size or for a # maximum duration before the resulting LogEvent is sent to the NotificationCenter. - attr_reader :batch_size, :zaius_manager, :logger + attr_reader :batch_size, :api_manager, :logger attr_accessor :odp_config def initialize( @@ -46,7 +47,7 @@ def initialize( # received signal should be sent after adding item to event_queue @received = ConditionVariable.new @logger = logger - @zaius_manager = api_manager || ZaiusRestApiManager.new(logger: @logger, proxy_config: proxy_config) + @api_manager = api_manager || OdpEventsApiManager.new(logger: @logger, proxy_config: proxy_config) @batch_size = Helpers::Constants::ODP_EVENT_MANAGER[:DEFAULT_BATCH_SIZE] @flush_interval = Helpers::Constants::ODP_EVENT_MANAGER[:DEFAULT_FLUSH_INTERVAL_SECONDS] @flush_deadline = 0 @@ -232,7 +233,7 @@ def flush_batch! i = 0 while i < @retry_count begin - should_retry = @zaius_manager.send_odp_events(@api_key, @api_host, @current_batch) + should_retry = @api_manager.send_odp_events(@api_key, @api_host, @current_batch) rescue StandardError => e should_retry = false @logger.log(Logger::ERROR, format(Helpers::Constants::ODP_LOGS[:ODP_EVENT_FAILED], "Error: #{e.message} #{@current_batch.to_json}")) diff --git a/lib/optimizely/odp/zaius_rest_api_manager.rb b/lib/optimizely/odp/odp_events_api_manager.rb similarity index 98% rename from lib/optimizely/odp/zaius_rest_api_manager.rb rename to lib/optimizely/odp/odp_events_api_manager.rb index 8e8d2fc4..cc4a307d 100644 --- a/lib/optimizely/odp/zaius_rest_api_manager.rb +++ b/lib/optimizely/odp/odp_events_api_manager.rb @@ -19,7 +19,7 @@ require 'json' module Optimizely - class ZaiusRestApiManager + class OdpEventsApiManager # Interface that handles sending ODP events. def initialize(logger: nil, proxy_config: nil) diff --git a/lib/optimizely/odp/odp_manager.rb b/lib/optimizely/odp/odp_manager.rb index d90deee9..d7521dcf 100644 --- a/lib/optimizely/odp/odp_manager.rb +++ b/lib/optimizely/odp/odp_manager.rb @@ -31,6 +31,7 @@ class OdpManager ODP_MANAGER_CONFIG = Helpers::Constants::ODP_MANAGER_CONFIG ODP_CONFIG_STATE = Helpers::Constants::ODP_CONFIG_STATE + # update_odp_config must be called to complete initialization def initialize(disable:, segments_cache: nil, segment_manager: nil, event_manager: nil, logger: nil) @enabled = !disable @segment_manager = segment_manager @@ -54,7 +55,6 @@ def initialize(disable:, segments_cache: nil, segment_manager: nil, event_manage @event_manager ||= Optimizely::OdpEventManager.new(logger: @logger) @segment_manager.odp_config = @odp_config - @event_manager.start!(@odp_config) end def fetch_qualified_segments(user_id:, options:) @@ -123,6 +123,7 @@ def send_event(type:, action:, identifiers:, data:) def update_odp_config(api_key, api_host, segments_to_check) # Update the odp config, reset the cache and send signal to the event processor to update its config. + # Start the event manager if odp is integrated. return unless @enabled config_changed = @odp_config.update(api_key, api_host, segments_to_check) @@ -132,10 +133,15 @@ def update_odp_config(api_key, api_host, segments_to_check) end @segment_manager.reset - @event_manager.update_config + + if @event_manager.running? + @event_manager.update_config + elsif @odp_config.odp_state == ODP_CONFIG_STATE[:INTEGRATED] + @event_manager.start!(@odp_config) + end end - def close! + def stop! return unless @enabled @event_manager.stop! diff --git a/lib/optimizely/odp/odp_segment_manager.rb b/lib/optimizely/odp/odp_segment_manager.rb index b89a4443..079684f3 100644 --- a/lib/optimizely/odp/odp_segment_manager.rb +++ b/lib/optimizely/odp/odp_segment_manager.rb @@ -17,18 +17,18 @@ # require 'optimizely/logger' -require_relative 'zaius_graphql_api_manager' +require_relative 'odp_segments_api_manager' module Optimizely class OdpSegmentManager # Schedules connections to ODP for audience segmentation and caches the results attr_accessor :odp_config - attr_reader :segments_cache, :zaius_manager, :logger + attr_reader :segments_cache, :api_manager, :logger def initialize(segments_cache, api_manager = nil, logger = nil, proxy_config = nil) @odp_config = nil @logger = logger || NoOpLogger.new - @zaius_manager = api_manager || ZaiusGraphQLApiManager.new(logger: @logger, proxy_config: proxy_config) + @api_manager = api_manager || OdpSegmentsApiManager.new(logger: @logger, proxy_config: proxy_config) @segments_cache = segments_cache end @@ -72,7 +72,7 @@ def fetch_qualified_segments(user_key, user_value, options) @logger.log(Logger::DEBUG, 'Making a call to ODP server.') - segments = @zaius_manager.fetch_segments(odp_api_key, odp_api_host, user_key, user_value, segments_to_check) + segments = @api_manager.fetch_segments(odp_api_key, odp_api_host, user_key, user_value, segments_to_check) @segments_cache.save(cache_key, segments) unless segments.nil? || ignore_cache segments end diff --git a/lib/optimizely/odp/zaius_graphql_api_manager.rb b/lib/optimizely/odp/odp_segments_api_manager.rb similarity index 99% rename from lib/optimizely/odp/zaius_graphql_api_manager.rb rename to lib/optimizely/odp/odp_segments_api_manager.rb index cabcaefd..136b9313 100644 --- a/lib/optimizely/odp/zaius_graphql_api_manager.rb +++ b/lib/optimizely/odp/odp_segments_api_manager.rb @@ -19,7 +19,7 @@ require 'json' module Optimizely - class ZaiusGraphQLApiManager + class OdpSegmentsApiManager # Interface that handles fetching audience segments. def initialize(logger: nil, proxy_config: nil) diff --git a/lib/optimizely/optimizely_factory.rb b/lib/optimizely/optimizely_factory.rb index 99ea733f..7fab1bfd 100644 --- a/lib/optimizely/optimizely_factory.rb +++ b/lib/optimizely/optimizely_factory.rb @@ -126,6 +126,7 @@ def self.default_instance_with_config_manager(config_manager) # @param user_profile_service - Optional UserProfileServiceInterface Provides methods to store and retreive user profiles. # @param config_manager - Optional ConfigManagerInterface Responds to 'config' method. # @param notification_center - Optional Instance of NotificationCenter. + # @param settings: Optional instance of OptimizelySdkSettings for sdk configuration. # # if @max_event_batch_size and @max_event_flush_interval are nil then default batchsize and flush_interval # will be used to setup batchEventProcessor. @@ -138,7 +139,8 @@ def self.custom_instance( # rubocop:disable Metrics/ParameterLists skip_json_validation = false, # rubocop:disable Style/OptionalBooleanParameter user_profile_service = nil, config_manager = nil, - notification_center = nil + notification_center = nil, + settings = nil ) error_handler ||= NoOpErrorHandler.new @@ -174,7 +176,9 @@ def self.custom_instance( # rubocop:disable Metrics/ParameterLists sdk_key, config_manager, notification_center, - event_processor + event_processor, + [], + settings ) end end diff --git a/lib/optimizely/optimizely_user_context.rb b/lib/optimizely/optimizely_user_context.rb index 1298fb7d..9b3b1d37 100644 --- a/lib/optimizely/optimizely_user_context.rb +++ b/lib/optimizely/optimizely_user_context.rb @@ -34,13 +34,15 @@ def initialize(optimizely_client, user_id, user_attributes) @user_id = user_id @user_attributes = user_attributes.nil? ? {} : user_attributes.clone @forced_decisions = {} - @qualified_segments = [] + @qualified_segments = nil + + @optimizely_client&.identify_user(user_id: user_id) end def clone user_context = OptimizelyUserContext.new(@optimizely_client, @user_id, user_attributes) @forced_decision_mutex.synchronize { user_context.instance_variable_set('@forced_decisions', @forced_decisions.dup) unless @forced_decisions.empty? } - @qualified_segment_mutex.synchronize { user_context.instance_variable_set('@qualified_segments', @qualified_segments.dup) unless @qualified_segments.empty? } + @qualified_segment_mutex.synchronize { user_context.instance_variable_set('@qualified_segments', @qualified_segments.dup) unless @qualified_segments.nil? } user_context end @@ -194,11 +196,43 @@ def qualified_segments=(segments) # Checks if user is qualified for the provided segment. # # @param segment - A segment name + # @return true if qualified. def qualified_for?(segment) - return false if @qualified_segments.empty? + qualified = false + @qualified_segment_mutex.synchronize do + break if @qualified_segments.nil? || @qualified_segments.empty? + + qualified = @qualified_segments.include?(segment) + end + qualified + end + + # Fetch all qualified segments for the user context. + # + # The segments fetched will be saved in `@qualified_segments` and can be accessed any time. + # + # @param options - A set of options for fetching qualified segments (optional). + # @param block - An optional block to call after segments have been fetched. + # If a block is provided, segments will be fetched on a separate thread. + # Block will be called with a boolean indicating if the fetch succeeded. + # @return If no block is provided, a boolean indicating whether the fetch was successful. + # Otherwise, returns a thread handle and the status boolean is passed to the block. - @qualified_segment_mutex.synchronize { @qualified_segments.include?(segment) } + def fetch_qualified_segments(options: [], &block) + fetch_segments = lambda do |opts, callback| + segments = @optimizely_client&.fetch_qualified_segments(user_id: @user_id, options: opts) + self.qualified_segments = segments + success = !segments.nil? + callback&.call(success) + success + end + + if block_given? + Thread.new(options, block, &fetch_segments) + else + fetch_segments.call(options, nil) + end end end end diff --git a/lib/optimizely/user_condition_evaluator.rb b/lib/optimizely/user_condition_evaluator.rb index ced8ebf7..af701616 100644 --- a/lib/optimizely/user_condition_evaluator.rb +++ b/lib/optimizely/user_condition_evaluator.rb @@ -331,7 +331,7 @@ def semver_less_than_or_equal_evaluator(condition) end def qualified_evaluator(condition) - # Evaluate the given match condition for the given user qaulified segments. + # Evaluate the given match condition for the given user qualified segments. # Returns boolean true if condition value is in the user's qualified segments, # false if the condition value is not in the user's qualified segments, # nil if the condition value isn't a string. diff --git a/spec/audience_spec.rb b/spec/audience_spec.rb index a531d8f9..73560aff 100644 --- a/spec/audience_spec.rb +++ b/spec/audience_spec.rb @@ -27,6 +27,7 @@ let(:integration_config) { Optimizely::DatafileProjectConfig.new(config_integration_JSON, spy_logger, error_handler) } let(:project_instance) { Optimizely::Project.new(config_body_JSON, nil, spy_logger, error_handler) } let(:user_context) { project_instance.create_user_context('some-user', {}) } + after(:example) { project_instance.close } it 'should return true for user_meets_audience_conditions? when experiment is using no audience' do # Both Audience Ids and Conditions are Empty diff --git a/spec/decision_service_spec.rb b/spec/decision_service_spec.rb index 3d4a687f..7646c032 100644 --- a/spec/decision_service_spec.rb +++ b/spec/decision_service_spec.rb @@ -30,6 +30,7 @@ let(:decision_service) { Optimizely::DecisionService.new(spy_logger, spy_user_profile_service) } let(:project_instance) { Optimizely::Project.new(config_body_JSON, nil, spy_logger, error_handler) } let(:user_context) { project_instance.create_user_context('some-user', {}) } + after(:example) { project_instance.close } describe '#get_variation' do before(:example) do @@ -889,7 +890,8 @@ bucketing_id, reason = decision_service.send(:get_bucketing_id, 'test_user', user_attributes) expect(bucketing_id).to eq('test_user') expect(reason).to eq(nil) - expect(spy_logger).not_to have_received(:log) + expect(spy_logger).not_to have_received(:log).with(Logger::WARN, anything) + expect(spy_logger).not_to have_received(:log).with(Logger::ERROR, anything) end it 'should not log any message and return given bucketing ID when bucketing ID is a String' do @@ -900,7 +902,8 @@ bucketing_id, reason = decision_service.send(:get_bucketing_id, 'test_user', user_attributes) expect(bucketing_id).to eq('i_am_bucketing_id') expect(reason).to eq(nil) - expect(spy_logger).not_to have_received(:log) + expect(spy_logger).not_to have_received(:log).with(Logger::WARN, anything) + expect(spy_logger).not_to have_received(:log).with(Logger::ERROR, anything) end it 'should not log any message and return empty String when bucketing ID is empty String' do @@ -911,7 +914,8 @@ bucketing_id, reason = decision_service.send(:get_bucketing_id, 'test_user', user_attributes) expect(bucketing_id).to eq('') expect(reason).to eq(nil) - expect(spy_logger).not_to have_received(:log) + expect(spy_logger).not_to have_received(:log).with(Logger::WARN, anything) + expect(spy_logger).not_to have_received(:log).with(Logger::ERROR, anything) end end diff --git a/spec/odp/odp_event_manager_spec.rb b/spec/odp/odp_event_manager_spec.rb index fa68a4a7..5dc11397 100644 --- a/spec/odp/odp_event_manager_spec.rb +++ b/spec/odp/odp_event_manager_spec.rb @@ -17,7 +17,7 @@ require 'optimizely/odp/odp_event' require 'optimizely/odp/lru_cache' require 'optimizely/odp/odp_config' -require 'optimizely/odp/zaius_rest_api_manager' +require 'optimizely/odp/odp_events_api_manager' require 'optimizely/logger' require 'optimizely/helpers/validator' @@ -98,18 +98,18 @@ it 'should return OdpEventManager instance' do config = Optimizely::OdpConfig.new - api_manager = Optimizely::ZaiusRestApiManager.new + api_manager = Optimizely::OdpEventsApiManager.new event_manager = Optimizely::OdpEventManager.new(api_manager: api_manager, logger: spy_logger) event_manager.start!(config) expect(event_manager.odp_config).to be config - expect(event_manager.zaius_manager).to be api_manager + expect(event_manager.api_manager).to be api_manager expect(event_manager.logger).to be spy_logger event_manager.stop! event_manager = Optimizely::OdpEventManager.new expect(event_manager.logger).to be_a Optimizely::NoOpLogger - expect(event_manager.zaius_manager).to be_a Optimizely::ZaiusRestApiManager + expect(event_manager.api_manager).to be_a Optimizely::OdpEventsApiManager end end @@ -135,7 +135,7 @@ it 'should flush at batch size' do allow(SecureRandom).to receive(:uuid).and_return(test_uuid) event_manager = Optimizely::OdpEventManager.new(logger: spy_logger) - allow(event_manager.zaius_manager).to receive(:send_odp_events).and_return(false) + allow(event_manager.api_manager).to receive(:send_odp_events).and_return(false) event_manager.start!(odp_config) event_manager.instance_variable_set('@batch_size', 2) @@ -155,7 +155,7 @@ allow(SecureRandom).to receive(:uuid).and_return(test_uuid) event_manager = Optimizely::OdpEventManager.new(logger: spy_logger) - allow(event_manager.zaius_manager).to receive(:send_odp_events).exactly(batch_count).times.and_return(false) + allow(event_manager.api_manager).to receive(:send_odp_events).exactly(batch_count).times.and_return(false) event_manager.start!(odp_config) event_manager.instance_variable_set('@batch_size', 2) @@ -181,7 +181,7 @@ event_manager.instance_variable_set('@batch_size', 2) batch_count = 4 - allow(event_manager.zaius_manager).to receive(:send_odp_events).exactly(batch_count).times.with(api_key, api_host, odp_events).and_return(false) + allow(event_manager.api_manager).to receive(:send_odp_events).exactly(batch_count).times.with(api_key, api_host, odp_events).and_return(false) # create events before starting processing to simulate backlog allow(event_manager).to receive(:running?).and_return(true) @@ -205,7 +205,7 @@ it 'should flush with flush signal' do allow(SecureRandom).to receive(:uuid).and_return(test_uuid) event_manager = Optimizely::OdpEventManager.new(logger: spy_logger) - allow(event_manager.zaius_manager).to receive(:send_odp_events).once.with(api_key, api_host, odp_events).and_return(false) + allow(event_manager.api_manager).to receive(:send_odp_events).once.with(api_key, api_host, odp_events).and_return(false) event_manager.start!(odp_config) event_manager.send_event(**events[0]) @@ -222,7 +222,7 @@ it 'should flush multiple times successfully' do allow(SecureRandom).to receive(:uuid).and_return(test_uuid) event_manager = Optimizely::OdpEventManager.new(logger: spy_logger) - allow(event_manager.zaius_manager).to receive(:send_odp_events).exactly(4).times.with(api_key, api_host, odp_events).and_return(false) + allow(event_manager.api_manager).to receive(:send_odp_events).exactly(4).times.with(api_key, api_host, odp_events).and_return(false) event_manager.start!(odp_config) flush_count = 4 @@ -246,7 +246,7 @@ allow(SecureRandom).to receive(:uuid).and_return(test_uuid) event_manager = Optimizely::OdpEventManager.new(logger: spy_logger) retry_count = event_manager.instance_variable_get('@retry_count') - allow(event_manager.zaius_manager).to receive(:send_odp_events).exactly(retry_count + 1).times.with(api_key, api_host, odp_events).and_return(true) + allow(event_manager.api_manager).to receive(:send_odp_events).exactly(retry_count + 1).times.with(api_key, api_host, odp_events).and_return(true) event_manager.start!(odp_config) event_manager.send_event(**events[0]) @@ -264,7 +264,7 @@ it 'should retry on network failure' do allow(SecureRandom).to receive(:uuid).and_return(test_uuid) event_manager = Optimizely::OdpEventManager.new(logger: spy_logger) - allow(event_manager.zaius_manager).to receive(:send_odp_events).once.with(api_key, api_host, odp_events).and_return(true, true, false) + allow(event_manager.api_manager).to receive(:send_odp_events).once.with(api_key, api_host, odp_events).and_return(true, true, false) event_manager.start!(odp_config) event_manager.send_event(**events[0]) @@ -282,7 +282,7 @@ it 'should log error on send failure' do allow(SecureRandom).to receive(:uuid).and_return(test_uuid) event_manager = Optimizely::OdpEventManager.new(logger: spy_logger) - allow(event_manager.zaius_manager).to receive(:send_odp_events).once.with(api_key, api_host, odp_events).and_raise(StandardError, 'Unexpected error') + allow(event_manager.api_manager).to receive(:send_odp_events).once.with(api_key, api_host, odp_events).and_raise(StandardError, 'Unexpected error') event_manager.start!(odp_config) event_manager.send_event(**events[0]) @@ -358,7 +358,7 @@ expect(odp_event.instance_variable_get('@data')[:data_source]).to eq 'my-app' - allow(event_manager.zaius_manager).to receive(:send_odp_events).once.with(api_key, api_host, [odp_event]).and_return(false) + allow(event_manager.api_manager).to receive(:send_odp_events).once.with(api_key, api_host, [odp_event]).and_return(false) event_manager.start!(odp_config) event_manager.send_event(**event) @@ -371,7 +371,7 @@ it 'should flush when timeout is reached' do allow(SecureRandom).to receive(:uuid).and_return(test_uuid) event_manager = Optimizely::OdpEventManager.new(logger: spy_logger) - allow(event_manager.zaius_manager).to receive(:send_odp_events).once.with(api_key, api_host, odp_events).and_return(false) + allow(event_manager.api_manager).to receive(:send_odp_events).once.with(api_key, api_host, odp_events).and_return(false) event_manager.instance_variable_set('@flush_interval', 0.5) event_manager.start!(odp_config) @@ -389,7 +389,7 @@ allow(SecureRandom).to receive(:uuid).and_return(test_uuid) odp_config = Optimizely::OdpConfig.new event_manager = Optimizely::OdpEventManager.new(logger: spy_logger) - allow(event_manager.zaius_manager).to receive(:send_odp_events).once.with(api_key, api_host, odp_events).and_return(false) + allow(event_manager.api_manager).to receive(:send_odp_events).once.with(api_key, api_host, odp_events).and_return(false) event_manager.start!(odp_config) event_manager.send_event(**events[0]) @@ -419,7 +419,7 @@ allow(SecureRandom).to receive(:uuid).and_return(test_uuid) odp_config = Optimizely::OdpConfig.new event_manager = Optimizely::OdpEventManager.new(logger: spy_logger) - expect(event_manager.zaius_manager).not_to receive(:send_odp_events) + expect(event_manager.api_manager).not_to receive(:send_odp_events) event_manager.start!(odp_config) event_manager.send_event(**events[0]) @@ -446,7 +446,7 @@ allow(SecureRandom).to receive(:uuid).and_return(test_uuid) odp_config = Optimizely::OdpConfig.new(api_key, api_host) event_manager = Optimizely::OdpEventManager.new(logger: spy_logger) - allow(event_manager.zaius_manager).to receive(:send_odp_events).once.with(api_key, api_host, odp_events).and_return(false) + allow(event_manager.api_manager).to receive(:send_odp_events).once.with(api_key, api_host, odp_events).and_return(false) event_manager.start!(odp_config) event_manager.instance_variable_set('@batch_size', 2) @@ -479,7 +479,7 @@ event_manager.odp_config = odp_config event_manager.instance_variable_set('@batch_size', 3) - allow(event_manager.zaius_manager).to receive(:send_odp_events).once.with(api_key, api_host, odp_events).and_return(false) + allow(event_manager.api_manager).to receive(:send_odp_events).once.with(api_key, api_host, odp_events).and_return(false) allow(event_manager).to receive(:running?).and_return(true) event_manager.send_event(**events[0]) event_manager.send_event(**events[1]) diff --git a/spec/odp/zaius_rest_api_manager_spec.rb b/spec/odp/odp_events_api_manager_spec.rb similarity index 90% rename from spec/odp/zaius_rest_api_manager_spec.rb rename to spec/odp/odp_events_api_manager_spec.rb index 5422c7c2..22a3225f 100644 --- a/spec/odp/zaius_rest_api_manager_spec.rb +++ b/spec/odp/odp_events_api_manager_spec.rb @@ -16,9 +16,9 @@ # limitations under the License. # require 'spec_helper' -require 'optimizely/odp/zaius_rest_api_manager' +require 'optimizely/odp/odp_events_api_manager' -describe Optimizely::ZaiusRestApiManager do +describe Optimizely::OdpEventsApiManager do let(:user_key) { 'vuid' } let(:user_value) { 'test-user-value' } let(:api_key) { 'test-api-key' } @@ -47,7 +47,7 @@ body: events.to_json ).to_return(status: 200) - api_manager = Optimizely::ZaiusRestApiManager.new + api_manager = Optimizely::OdpEventsApiManager.new expect(spy_logger).not_to receive(:log) should_retry = api_manager.send_odp_events(api_key, api_host, events) @@ -56,7 +56,7 @@ it 'should return true on network error' do allow(Optimizely::Helpers::HttpUtils).to receive(:make_request).and_raise(SocketError) - api_manager = Optimizely::ZaiusRestApiManager.new(logger: spy_logger) + api_manager = Optimizely::OdpEventsApiManager.new(logger: spy_logger) expect(spy_logger).to receive(:log).with(Logger::ERROR, 'ODP event send failed (network error).') should_retry = api_manager.send_odp_events(api_key, api_host, events) @@ -70,7 +70,7 @@ body: events.to_json ).to_return(status: [400, 'Bad Request'], body: failure_response_data) - api_manager = Optimizely::ZaiusRestApiManager.new(logger: spy_logger) + api_manager = Optimizely::OdpEventsApiManager.new(logger: spy_logger) expect(spy_logger).to receive(:log).with( Logger::ERROR, 'ODP event send failed ({"title":"Bad Request","status":400,' \ '"timestamp":"2022-07-01T20:44:00.945Z","detail":{"invalids":' \ @@ -88,7 +88,7 @@ body: events.to_json ).to_return(status: [500, 'Internal Server Error']) - api_manager = Optimizely::ZaiusRestApiManager.new(logger: spy_logger) + api_manager = Optimizely::OdpEventsApiManager.new(logger: spy_logger) expect(spy_logger).to receive(:log).with(Logger::ERROR, 'ODP event send failed (500: Internal Server Error).') should_retry = api_manager.send_odp_events(api_key, api_host, events) diff --git a/spec/odp/odp_manager_spec.rb b/spec/odp/odp_manager_spec.rb index 6fb2d204..a9721a8f 100644 --- a/spec/odp/odp_manager_spec.rb +++ b/spec/odp/odp_manager_spec.rb @@ -18,7 +18,7 @@ require 'optimizely/odp/odp_event' require 'optimizely/odp/lru_cache' require 'optimizely/odp/odp_config' -require 'optimizely/odp/zaius_rest_api_manager' +require 'optimizely/odp/odp_events_api_manager' require 'optimizely/logger' require 'optimizely/helpers/validator' require 'optimizely/helpers/constants' @@ -46,9 +46,8 @@ event_manager = manager.instance_variable_get('@event_manager') expect(event_manager).to be_a Optimizely::OdpEventManager - expect(event_manager.odp_config).to be odp_config expect(event_manager.logger).to be logger - expect(event_manager.running?).to be true + expect(event_manager.running?).to be false segment_manager = manager.instance_variable_get('@segment_manager') expect(segment_manager).to be_a Optimizely::OdpSegmentManager @@ -59,9 +58,6 @@ expect(segments_cache).to be_a Optimizely::LRUCache expect(segments_cache.instance_variable_get('@capacity')).to eq 10_000 expect(segments_cache.instance_variable_get('@timeout')).to eq 600 - - manager.close! - expect(event_manager.running?).to be false end it 'should allow custom segment_manager' do @@ -73,7 +69,7 @@ expect(manager.instance_variable_get('@segment_manager')).to be segment_manager expect(manager.instance_variable_get('@segment_manager').instance_variable_get('@segments_cache')).to be segments_cache - manager.close! + manager.stop! end it 'should allow custom segments_cache' do @@ -83,7 +79,7 @@ expect(manager.instance_variable_get('@segment_manager').instance_variable_get('@segments_cache')).to be segments_cache - manager.close! + manager.stop! end it 'should allow custom event_manager' do @@ -93,7 +89,7 @@ expect(manager.instance_variable_get('@event_manager')).to be event_manager - manager.close! + manager.stop! end it 'should not instantiate event/segment managers when disabled' do @@ -120,7 +116,7 @@ segments = manager.fetch_qualified_segments(user_id: user_value, options: nil) expect(segments).to eq [segments_to_check[0]] - manager.close! + manager.stop! end it 'should log error if disabled' do @@ -137,7 +133,7 @@ response = manager.fetch_qualified_segments(user_id: 'user1', options: nil) expect(response).to be_nil - manager.close! + manager.stop! end it 'should ignore cache' do @@ -145,7 +141,7 @@ expect(spy_logger).not_to receive(:log).with(Logger::ERROR, anything) segment_manager = Optimizely::OdpSegmentManager.new(segments_cache, nil, spy_logger) - expect(segment_manager.zaius_manager) + expect(segment_manager.api_manager) .to receive(:fetch_segments) .once .with(api_key, api_host, user_key, user_value, segments_to_check) @@ -160,7 +156,7 @@ segments = manager.fetch_qualified_segments(user_id: user_value, options: [Optimizely::OptimizelySegmentOption::IGNORE_CACHE]) expect(segments).to eq [segments_to_check[0]] - manager.close! + manager.stop! end it 'should reset cache' do @@ -168,7 +164,7 @@ segment_manager = Optimizely::OdpSegmentManager.new(segments_cache) expect(spy_logger).not_to receive(:log).with(Logger::ERROR, anything) - expect(segment_manager.zaius_manager) + expect(segment_manager.api_manager) .to receive(:fetch_segments) .once .with(api_key, api_host, user_key, user_value, segments_to_check) @@ -184,7 +180,7 @@ expect(segments).to eq [segments_to_check[0]] expect(segments_cache.lookup('wow')).to be_nil - manager.close! + manager.stop! end end @@ -194,7 +190,7 @@ event_manager = Optimizely::OdpEventManager.new expect(spy_logger).not_to receive(:log).with(Logger::ERROR, anything) - expect(event_manager.zaius_manager) + expect(event_manager.api_manager) .to receive(:send_odp_events) .once .with(api_key, api_host, [odp_event]) @@ -205,7 +201,7 @@ manager.send_event(**event) - manager.close! + manager.stop! end it 'should log error if data is invalid' do @@ -217,7 +213,7 @@ manager.send_event(**event) - manager.close! + manager.stop! end end @@ -228,7 +224,7 @@ event = Optimizely::OdpEvent.new(type: 'fullstack', action: 'identified', identifiers: {user_key => user_value}, data: {}) expect(spy_logger).not_to receive(:log).with(Logger::ERROR, anything) - expect(event_manager.zaius_manager) + expect(event_manager.api_manager) .to receive(:send_odp_events) .once .with(api_key, api_host, [event]) @@ -239,7 +235,7 @@ manager.identify_user(user_id: user_value) - manager.close! + manager.stop! end it 'should log debug if disabled' do @@ -249,7 +245,7 @@ manager = Optimizely::OdpManager.new(disable: true, logger: spy_logger) manager.identify_user(user_id: user_value) - manager.close! + manager.stop! end it 'should log debug if not integrated' do @@ -259,7 +255,7 @@ manager.update_odp_config(nil, nil, []) manager.identify_user(user_id: user_value) - manager.close! + manager.stop! end it 'should log debug if datafile not ready' do @@ -269,20 +265,25 @@ manager = Optimizely::OdpManager.new(disable: false, logger: spy_logger) manager.identify_user(user_id: user_value) - manager.close! + manager.stop! end end describe '#update_odp_config' do - it 'update config' do + it 'update config and start event_manager' do expect(spy_logger).not_to receive(:log).with(Logger::ERROR, anything) manager = Optimizely::OdpManager.new(disable: false, logger: spy_logger) + + event_manager = manager.instance_variable_get('@event_manager') + expect(event_manager.running?).to be false + segment_manager = manager.instance_variable_get('@segment_manager') segments_cache = segment_manager.instance_variable_get('@segments_cache') segments_cache.save('wow', 'great') expect(segments_cache.lookup('wow')).to eq 'great' manager.update_odp_config(api_key, api_host, segments_to_check) + expect(event_manager.running?).to be true manager_config = manager.instance_variable_get('@odp_config') expect(manager_config.api_host).to eq api_host @@ -296,7 +297,6 @@ # confirm cache was reset expect(segments_cache.lookup('wow')).to be_nil - event_manager = manager.instance_variable_get('@event_manager') sleep(0.1) until event_manager.instance_variable_get('@event_queue').empty? event_manager_config = event_manager.odp_config expect(event_manager_config.api_host).to eq api_host @@ -306,7 +306,7 @@ expect(event_manager.instance_variable_get('@api_host')).to eq api_host expect(event_manager.instance_variable_get('@api_key')).to eq api_key - manager.close! + manager.stop! end end end diff --git a/spec/odp/odp_segment_manager_spec.rb b/spec/odp/odp_segment_manager_spec.rb index 4910eea4..6ba1a0ac 100644 --- a/spec/odp/odp_segment_manager_spec.rb +++ b/spec/odp/odp_segment_manager_spec.rb @@ -16,7 +16,7 @@ require 'optimizely/odp/odp_segment_manager' require 'optimizely/odp/lru_cache' require 'optimizely/odp/odp_config' -require 'optimizely/odp/zaius_graphql_api_manager' +require 'optimizely/odp/odp_segments_api_manager' require 'optimizely/logger' describe Optimizely::OdpSegmentManager do @@ -63,18 +63,18 @@ describe '#initialize' do it 'should return OdpSegmentManager instance' do - api_manager = Optimizely::ZaiusGraphQLApiManager.new + api_manager = Optimizely::OdpSegmentsApiManager.new segment_manager = Optimizely::OdpSegmentManager.new(segments_cache, api_manager, spy_logger) expect(segment_manager.segments_cache).to be_a Optimizely::LRUCache expect(segment_manager.segments_cache).to be segments_cache expect(segment_manager.odp_config).to be nil - expect(segment_manager.zaius_manager).to be api_manager + expect(segment_manager.api_manager).to be api_manager expect(segment_manager.logger).to be spy_logger segment_manager = Optimizely::OdpSegmentManager.new(segments_cache) expect(segment_manager.logger).to be_a Optimizely::NoOpLogger - expect(segment_manager.zaius_manager).to be_a Optimizely::ZaiusGraphQLApiManager + expect(segment_manager.api_manager).to be_a Optimizely::OdpSegmentsApiManager end end diff --git a/spec/odp/zaius_graphql_api_manager_spec.rb b/spec/odp/odp_segments_api_manager_spec.rb similarity index 85% rename from spec/odp/zaius_graphql_api_manager_spec.rb rename to spec/odp/odp_segments_api_manager_spec.rb index 72ae67e1..ab0b7a0e 100644 --- a/spec/odp/zaius_graphql_api_manager_spec.rb +++ b/spec/odp/odp_segments_api_manager_spec.rb @@ -16,16 +16,16 @@ # limitations under the License. # require 'spec_helper' -require 'optimizely/odp/zaius_graphql_api_manager' +require 'optimizely/odp/odp_segments_api_manager' -describe Optimizely::ZaiusGraphQLApiManager do +describe Optimizely::OdpSegmentsApiManager do let(:user_key) { 'vuid' } let(:user_value) { 'test-user-value' } let(:api_key) { 'test-api-key' } let(:api_host) { 'https://test-host' } let(:error_handler) { Optimizely::RaiseErrorHandler.new } let(:spy_logger) { spy('logger') } - let(:zaius_manager) { Optimizely::ZaiusGraphQLApiManager.new(logger: spy_logger) } + let(:api_manager) { Optimizely::OdpSegmentsApiManager.new(logger: spy_logger) } let(:good_response_data) do { data: { @@ -228,7 +228,7 @@ ) .to_return(status: 200, body: good_response_data.to_json) - segments = zaius_manager.fetch_segments(api_key, api_host, user_key, user_value, %w[a b c]) + segments = api_manager.fetch_segments(api_key, api_host, user_key, user_value, %w[a b c]) expect(segments).to match_array %w[a b] end @@ -236,7 +236,7 @@ stub_request(:post, "#{api_host}/v3/graphql") .to_return(status: 200, body: good_empty_response_data.to_json) - segments = zaius_manager.fetch_segments(api_key, api_host, user_key, user_value, []) + segments = api_manager.fetch_segments(api_key, api_host, user_key, user_value, []) expect(segments).to match_array [] end @@ -244,7 +244,7 @@ stub_request(:post, "#{api_host}/v3/graphql") .to_return(status: 200, body: node_missing_response_data.to_json) - segments = zaius_manager.fetch_segments(api_key, api_host, user_key, user_value, %w[a b]) + segments = api_manager.fetch_segments(api_key, api_host, user_key, user_value, %w[a b]) expect(segments).to be_nil expect(spy_logger).to have_received(:log).once.with( @@ -257,7 +257,7 @@ stub_request(:post, "#{api_host}/v3/graphql") .to_return(status: 200, body: mixed_missing_keys_response_data.to_json) - segments = zaius_manager.fetch_segments(api_key, api_host, user_key, user_value, %w[a b]) + segments = api_manager.fetch_segments(api_key, api_host, user_key, user_value, %w[a b]) expect(segments).to be_nil expect(spy_logger).to have_received(:log).once.with( @@ -270,7 +270,7 @@ stub_request(:post, "#{api_host}/v3/graphql") .to_return(status: 200, body: invalid_identifier_response_data.to_json) - segments = zaius_manager.fetch_segments(api_key, api_host, user_key, user_value, %w[a b]) + segments = api_manager.fetch_segments(api_key, api_host, user_key, user_value, %w[a b]) expect(segments).to be_nil expect(spy_logger).to have_received(:log).once.with( @@ -283,7 +283,7 @@ stub_request(:post, "#{api_host}/v3/graphql") .to_return(status: 200, body: other_exception_response_data.to_json) - segments = zaius_manager.fetch_segments(api_key, api_host, user_key, user_value, %w[a b]) + segments = api_manager.fetch_segments(api_key, api_host, user_key, user_value, %w[a b]) expect(segments).to be_nil expect(spy_logger).to have_received(:log).once.with( @@ -296,7 +296,7 @@ stub_request(:post, "#{api_host}/v3/graphql") .to_return(status: 200, body: bad_response_data.to_json) - segments = zaius_manager.fetch_segments(api_key, api_host, user_key, user_value, %w[a b]) + segments = api_manager.fetch_segments(api_key, api_host, user_key, user_value, %w[a b]) expect(segments).to be_nil expect(spy_logger).to have_received(:log).once.with( @@ -309,7 +309,7 @@ stub_request(:post, "#{api_host}/v3/graphql") .to_return(status: 200, body: name_invalid_response_data) - segments = zaius_manager.fetch_segments(api_key, api_host, user_key, user_value, %w[a b]) + segments = api_manager.fetch_segments(api_key, api_host, user_key, user_value, %w[a b]) expect(segments).to be_nil expect(spy_logger).to have_received(:log).once.with( @@ -322,7 +322,7 @@ stub_request(:post, "#{api_host}/v3/graphql") .to_return(status: 200, body: invalid_edges_key_response_data.to_json) - segments = zaius_manager.fetch_segments(api_key, api_host, user_key, user_value, %w[a b]) + segments = api_manager.fetch_segments(api_key, api_host, user_key, user_value, %w[a b]) expect(segments).to be_nil expect(spy_logger).to have_received(:log).once.with( @@ -335,7 +335,7 @@ stub_request(:post, "#{api_host}/v3/graphql") .to_return(status: 200, body: invalid_key_for_error_response_data.to_json) - segments = zaius_manager.fetch_segments(api_key, api_host, user_key, user_value, %w[a b]) + segments = api_manager.fetch_segments(api_key, api_host, user_key, user_value, %w[a b]) expect(segments).to be_nil expect(spy_logger).to have_received(:log).once.with( @@ -348,7 +348,7 @@ stub_request(:post, "#{api_host}/v3/graphql") .and_raise(SocketError) - segments = zaius_manager.fetch_segments(api_key, api_host, user_key, user_value, %w[a b]) + segments = api_manager.fetch_segments(api_key, api_host, user_key, user_value, %w[a b]) expect(segments).to be_nil expect(spy_logger).to have_received(:log).once.with( @@ -366,7 +366,7 @@ stub_request(:post, "#{api_host}/v3/graphql") .to_return(status: 400) - segments = zaius_manager.fetch_segments(api_key, api_host, user_key, user_value, %w[a b]) + segments = api_manager.fetch_segments(api_key, api_host, user_key, user_value, %w[a b]) expect(segments).to be_nil expect(spy_logger).to have_received(:log).once.with( @@ -379,7 +379,7 @@ stub_request(:post, "#{api_host}/v3/graphql") .to_return(status: 500) - segments = zaius_manager.fetch_segments(api_key, api_host, user_key, user_value, %w[a b]) + segments = api_manager.fetch_segments(api_key, api_host, user_key, user_value, %w[a b]) expect(segments).to be_nil expect(spy_logger).to have_received(:log).once.with( @@ -396,7 +396,7 @@ '{audiences(subset:[]) {edges {node {name state}}}}}' } ) - zaius_manager.fetch_segments(api_key, api_host, user_key, user_value, nil) + api_manager.fetch_segments(api_key, api_host, user_key, user_value, nil) stub_request(:post, "#{api_host}/v3/graphql") .with( @@ -405,7 +405,7 @@ '{audiences(subset:[]) {edges {node {name state}}}}}' } ) - zaius_manager.fetch_segments(api_key, api_host, user_key, user_value, []) + api_manager.fetch_segments(api_key, api_host, user_key, user_value, []) stub_request(:post, "#{api_host}/v3/graphql") .with( @@ -414,7 +414,7 @@ '{audiences(subset:["a"]) {edges {node {name state}}}}}' } ) - zaius_manager.fetch_segments(api_key, api_host, user_key, user_value, %w[a]) + api_manager.fetch_segments(api_key, api_host, user_key, user_value, %w[a]) stub_request(:post, "#{api_host}/v3/graphql") .with( @@ -423,15 +423,15 @@ '{audiences(subset:["a", "b", "c"]) {edges {node {name state}}}}}' } ) - zaius_manager.fetch_segments(api_key, api_host, user_key, user_value, %w[a b c]) + api_manager.fetch_segments(api_key, api_host, user_key, user_value, %w[a b c]) end it 'should pass the proxy config that is passed in' do allow(Optimizely::Helpers::HttpUtils).to receive(:make_request).and_raise(SocketError) stub_request(:post, "#{api_host}/v3/graphql") - zaius_manager = Optimizely::ZaiusGraphQLApiManager.new(logger: spy_logger, proxy_config: :proxy_config) - zaius_manager.fetch_segments(api_key, api_host, user_key, user_value, []) + api_manager = Optimizely::OdpSegmentsApiManager.new(logger: spy_logger, proxy_config: :proxy_config) + api_manager.fetch_segments(api_key, api_host, user_key, user_value, []) expect(Optimizely::Helpers::HttpUtils).to have_received(:make_request).with(anything, anything, anything, anything, anything, :proxy_config) end end diff --git a/spec/optimizely_config_spec.rb b/spec/optimizely_config_spec.rb index f95e8f10..8d364e1d 100644 --- a/spec/optimizely_config_spec.rb +++ b/spec/optimizely_config_spec.rb @@ -37,6 +37,12 @@ let(:project_config_similar_rule_keys) { Optimizely::DatafileProjectConfig.new(similar_rule_key_JSON, spy_logger, error_handler) } let(:project_instance_similar_rule_keys) { Optimizely::Project.new(similar_rule_key_JSON, nil, spy_logger, error_handler) } let(:optimizely_config_similar_rule_keys) { project_instance_similar_rule_keys.get_optimizely_config } + after(:example) do + project_instance.close + project_instance_sim_keys.close + project_instance_typed_audiences.close + project_instance_similar_rule_keys.close + end it 'should return all experiments' do experiments_map = optimizely_config['experimentsMap'] diff --git a/spec/optimizely_factory_spec.rb b/spec/optimizely_factory_spec.rb index c293f5da..c875fec1 100644 --- a/spec/optimizely_factory_spec.rb +++ b/spec/optimizely_factory_spec.rb @@ -29,6 +29,8 @@ let(:user_profile_service) { spy('user_profile_service') } let(:event_dispatcher) { Optimizely::EventDispatcher.new } let(:notification_center) { Optimizely::NotificationCenter.new(spy_logger, error_handler) } + let(:config_body_integrations) { OptimizelySpec::CONFIG_DICT_WITH_INTEGRATIONS } + let(:config_body_integrations_JSON) { OptimizelySpec::CONFIG_DICT_WITH_INTEGRATIONS_JSON } before(:example) do WebMock.allow_net_connect! @@ -131,6 +133,21 @@ class CustomConfigManager # rubocop:disable Lint/ConstantDefinitionInBlock expect(logger).to be(optimizely_instance.logger) expect(notification_center).to be(optimizely_instance.notification_center) end + + it 'should update odp_config correctly' do + stub_request(:get, 'https://cdn.optimizely.com/datafiles/instance-test.json') + .to_return(status: 200, body: config_body_integrations_JSON) + project = Optimizely::OptimizelyFactory.custom_instance('instance-test') + + # wait for config to be ready + project.config_manager.config + + odp_config = project.instance_variable_get('@odp_manager').instance_variable_get('@odp_config') + expect(odp_config.api_key).to eq config_body_integrations['integrations'][0]['publicKey'] + expect(odp_config.api_host).to eq config_body_integrations['integrations'][0]['host'] + + project.close + end end describe '.max_event_batch_size' do diff --git a/spec/optimizely_user_context_spec.rb b/spec/optimizely_user_context_spec.rb index 3c3be2e4..4a28dc6b 100644 --- a/spec/optimizely_user_context_spec.rb +++ b/spec/optimizely_user_context_spec.rb @@ -31,6 +31,77 @@ let(:forced_decision_project_instance) { Optimizely::Project.new(forced_decision_JSON, nil, spy_logger, error_handler) } let(:integration_project_instance) { Optimizely::Project.new(integration_JSON, nil, spy_logger, error_handler) } let(:impression_log_url) { 'https://logx.optimizely.com/v1/events' } + let(:good_response_data) do + { + data: { + customer: { + audiences: { + edges: [ + { + node: { + name: 'a', + state: 'qualified', + description: 'qualifed sample 1' + } + }, + { + node: { + name: 'b', + state: 'qualified', + description: 'qualifed sample 2' + } + }, + { + node: { + name: 'c', + state: 'not_qualified', + description: 'not-qualified sample' + } + } + ] + } + } + } + } + end + let(:integrated_response_data) do + { + data: { + customer: { + audiences: { + edges: [ + { + node: { + name: 'odp-segment-1', + state: 'qualified', + description: 'qualifed sample 1' + } + }, + { + node: { + name: 'odp-segment-none', + state: 'qualified', + description: 'qualifed sample 2' + } + }, + { + node: { + name: 'odp-segment-2', + state: 'not_qualified', + description: 'not-qualified sample' + } + } + ] + } + } + } + } + end + after(:example) do + project_instance.close + forced_decision_project_instance.close + integration_project_instance.close + end describe '#initialize' do it 'should set passed value as expected' do @@ -47,6 +118,11 @@ user_context_obj = Optimizely::OptimizelyUserContext.new(project_instance, 'test_user', nil) expect(user_context_obj.instance_variable_get(:@user_attributes)).to eq({}) end + + it 'should not fail with a nil client' do + user_context_obj = Optimizely::OptimizelyUserContext.new(nil, 'test-user', nil) + expect(user_context_obj).to be_a Optimizely::OptimizelyUserContext + end end describe '#set_attribute' do @@ -725,6 +801,7 @@ end end it 'should clone qualified segments in user context' do + stub_request(:post, 'https://api.zaius.com/v3/events').to_return(status: 200) user_context_obj = Optimizely::OptimizelyUserContext.new(integration_project_instance, 'tester', {}) qualified_segments = %w[seg1 seg2] user_context_obj.qualified_segments = qualified_segments @@ -734,64 +811,245 @@ expect(user_clone_1.qualified_segments).to eq qualified_segments expect(user_clone_1.qualified_segments).not_to be user_context_obj.qualified_segments expect(user_clone_1.qualified_segments).not_to be qualified_segments + integration_project_instance.close end it 'should hit segment in ab test' do stub_request(:post, impression_log_url) + stub_request(:post, 'https://api.zaius.com/v3/events').to_return(status: 200) user_context_obj = Optimizely::OptimizelyUserContext.new(integration_project_instance, 'tester', {}) user_context_obj.qualified_segments = %w[odp-segment-1 odp-segment-none] decision = user_context_obj.decide('flag-segment') expect(decision.variation_key).to eq 'variation-a' + integration_project_instance.close end it 'should hit other audience with segments in ab test' do stub_request(:post, impression_log_url) + stub_request(:post, 'https://api.zaius.com/v3/events').to_return(status: 200) user_context_obj = Optimizely::OptimizelyUserContext.new(integration_project_instance, 'tester', 'age' => 30) user_context_obj.qualified_segments = %w[odp-segment-none] decision = user_context_obj.decide('flag-segment', [Optimizely::Decide::OptimizelyDecideOption::IGNORE_USER_PROFILE_SERVICE]) expect(decision.variation_key).to eq 'variation-a' + integration_project_instance.close end it 'should hit segment in rollout' do stub_request(:post, impression_log_url) + stub_request(:post, 'https://api.zaius.com/v3/events').to_return(status: 200) user_context_obj = Optimizely::OptimizelyUserContext.new(integration_project_instance, 'tester', {}) user_context_obj.qualified_segments = %w[odp-segment-2] decision = user_context_obj.decide('flag-segment', [Optimizely::Decide::OptimizelyDecideOption::IGNORE_USER_PROFILE_SERVICE]) expect(decision.variation_key).to eq 'rollout-variation-on' + integration_project_instance.close end it 'should miss segment in rollout' do stub_request(:post, impression_log_url) + stub_request(:post, 'https://api.zaius.com/v3/events').to_return(status: 200) user_context_obj = Optimizely::OptimizelyUserContext.new(integration_project_instance, 'tester', {}) user_context_obj.qualified_segments = %w[odp-segment-none] decision = user_context_obj.decide('flag-segment', [Optimizely::Decide::OptimizelyDecideOption::IGNORE_USER_PROFILE_SERVICE]) expect(decision.variation_key).to eq 'rollout-variation-off' + integration_project_instance.close end it 'should miss segment with empty segments' do stub_request(:post, impression_log_url) + stub_request(:post, 'https://api.zaius.com/v3/events').to_return(status: 200) user_context_obj = Optimizely::OptimizelyUserContext.new(integration_project_instance, 'tester', {}) user_context_obj.qualified_segments = [] decision = user_context_obj.decide('flag-segment', [Optimizely::Decide::OptimizelyDecideOption::IGNORE_USER_PROFILE_SERVICE]) expect(decision.variation_key).to eq 'rollout-variation-off' + integration_project_instance.close end it 'should not fail without any segments' do stub_request(:post, impression_log_url) + stub_request(:post, 'https://api.zaius.com/v3/events').to_return(status: 200) user_context_obj = Optimizely::OptimizelyUserContext.new(integration_project_instance, 'tester', {}) decision = user_context_obj.decide('flag-segment', [Optimizely::Decide::OptimizelyDecideOption::IGNORE_USER_PROFILE_SERVICE]) expect(decision.variation_key).to eq 'rollout-variation-off' + integration_project_instance.close + end + + it 'should send identify event when user context created' do + stub_request(:post, 'https://api.zaius.com/v3/graphql').to_return(status: 200, body: good_response_data.to_json) + stub_request(:post, 'https://api.zaius.com/v3/events').to_return(status: 200) + expect(integration_project_instance.odp_manager).to receive(:identify_user).with({user_id: 'tester'}) + Optimizely::OptimizelyUserContext.new(integration_project_instance, 'tester', {}) + + integration_project_instance.close + end + + describe '#fetch_qualified_segments' do + it 'should fetch segments' do + stub_request(:post, 'https://api.zaius.com/v3/graphql').to_return(status: 200, body: good_response_data.to_json) + stub_request(:post, 'https://api.zaius.com/v3/events').to_return(status: 200) + user_context_obj = Optimizely::OptimizelyUserContext.new(integration_project_instance, 'tester', {}) + + success = user_context_obj.fetch_qualified_segments + + expect(user_context_obj.qualified_segments).to eq %w[a b] + expect(success).to be true + integration_project_instance.close + end + + it 'should save empty array when not qualified for any segments' do + good_response_data[:data][:customer][:audiences][:edges].map { |e| e[:node][:state] = 'unqualified' } + + stub_request(:post, 'https://api.zaius.com/v3/graphql').to_return(status: 200, body: good_response_data.to_json) + stub_request(:post, 'https://api.zaius.com/v3/events').to_return(status: 200) + user_context_obj = Optimizely::OptimizelyUserContext.new(integration_project_instance, 'tester', {}) + + success = user_context_obj.fetch_qualified_segments + + expect(user_context_obj.qualified_segments).to eq [] + expect(success).to be true + integration_project_instance.close + end + + it 'should fetch segments and reset cache' do + stub_request(:post, 'https://api.zaius.com/v3/graphql').to_return(status: 200, body: good_response_data.to_json) + stub_request(:post, 'https://api.zaius.com/v3/events').to_return(status: 200) + segments_cache = integration_project_instance.odp_manager.instance_variable_get('@segment_manager').instance_variable_get('@segments_cache') + segments_cache.save('wow', 'great') + expect(segments_cache.lookup('wow')).to eq 'great' + user_context_obj = Optimizely::OptimizelyUserContext.new(integration_project_instance, 'tester', {}) + + success = user_context_obj.fetch_qualified_segments(options: [:RESET_CACHE]) + + expect(segments_cache.lookup('wow')).to be_nil + expect(user_context_obj.qualified_segments).to eq %w[a b] + expect(success).to be true + integration_project_instance.close + end + + it 'should fetch segments from cache' do + stub_request(:post, 'https://api.zaius.com/v3/graphql').to_return(status: 200, body: good_response_data.to_json) + stub_request(:post, 'https://api.zaius.com/v3/events').to_return(status: 200) + + segment_manager = integration_project_instance.odp_manager.instance_variable_get('@segment_manager') + cache_key = segment_manager.send(:make_cache_key, Optimizely::Helpers::Constants::ODP_MANAGER_CONFIG[:KEY_FOR_USER_ID], 'tester') + + segments_cache = segment_manager.instance_variable_get('@segments_cache') + segments_cache.save(cache_key, %w[great]) + expect(segments_cache.lookup(cache_key)).to eq %w[great] + + user_context_obj = Optimizely::OptimizelyUserContext.new(integration_project_instance, 'tester', {}) + success = user_context_obj.fetch_qualified_segments + + expect(user_context_obj.qualified_segments).to eq %w[great] + expect(success).to be true + integration_project_instance.close + end + + it 'should fetch segments and ignore cache' do + stub_request(:post, 'https://api.zaius.com/v3/graphql').to_return(status: 200, body: good_response_data.to_json) + stub_request(:post, 'https://api.zaius.com/v3/events').to_return(status: 200) + + segment_manager = integration_project_instance.odp_manager.instance_variable_get('@segment_manager') + cache_key = segment_manager.send(:make_cache_key, Optimizely::Helpers::Constants::ODP_MANAGER_CONFIG[:KEY_FOR_USER_ID], 'tester') + + segments_cache = segment_manager.instance_variable_get('@segments_cache') + segments_cache.save(cache_key, %w[great]) + expect(segments_cache.lookup(cache_key)).to eq %w[great] + + user_context_obj = Optimizely::OptimizelyUserContext.new(integration_project_instance, 'tester', {}) + success = user_context_obj.fetch_qualified_segments(options: [:IGNORE_CACHE]) + + expect(user_context_obj.qualified_segments).to eq %w[a b] + expect(success).to be true + expect(segments_cache.lookup(cache_key)).to eq %w[great] + integration_project_instance.close + end + + it 'should return false on error' do + stub_request(:post, 'https://api.zaius.com/v3/graphql').to_return(status: 500) + stub_request(:post, 'https://api.zaius.com/v3/events').to_return(status: 200) + user_context_obj = Optimizely::OptimizelyUserContext.new(integration_project_instance, 'tester', {}) + + success = user_context_obj.fetch_qualified_segments + + expect(user_context_obj.qualified_segments).to be_nil + expect(success).to be false + integration_project_instance.close + end + + it 'should not raise error with a nil client' do + user_context_obj = Optimizely::OptimizelyUserContext.new(nil, 'tester', {}) + user_context_obj.fetch_qualified_segments + end + + it 'should fetch segments when non-blocking' do + stub_request(:post, 'https://api.zaius.com/v3/graphql').to_return(status: 200, body: good_response_data.to_json) + stub_request(:post, 'https://api.zaius.com/v3/events').to_return(status: 200) + user_context_obj = Optimizely::OptimizelyUserContext.new(integration_project_instance, 'tester', {}) + + user_context_obj.fetch_qualified_segments do |success| + expect(success).to be true + expect(user_context_obj.qualified_segments).to eq %w[a b] + integration_project_instance.close + end + end + + it 'should pass false to callback when failed and non-blocking' do + stub_request(:post, 'https://api.zaius.com/v3/graphql').to_return(status: 500) + stub_request(:post, 'https://api.zaius.com/v3/events').to_return(status: 200) + user_context_obj = Optimizely::OptimizelyUserContext.new(integration_project_instance, 'tester', {}) + + thread = user_context_obj.fetch_qualified_segments do |success| + expect(success).to be false + expect(user_context_obj.qualified_segments).to be_nil + end + thread.join + integration_project_instance.close + end + + it 'should fetch segments from cache with non-blocking' do + stub_request(:post, 'https://api.zaius.com/v3/graphql').to_return(status: 200, body: good_response_data.to_json) + stub_request(:post, 'https://api.zaius.com/v3/events').to_return(status: 200) + + segment_manager = integration_project_instance.odp_manager.instance_variable_get('@segment_manager') + cache_key = segment_manager.send(:make_cache_key, Optimizely::Helpers::Constants::ODP_MANAGER_CONFIG[:KEY_FOR_USER_ID], 'tester') + + segments_cache = segment_manager.instance_variable_get('@segments_cache') + segments_cache.save(cache_key, %w[great]) + expect(segments_cache.lookup(cache_key)).to eq %w[great] + + user_context_obj = Optimizely::OptimizelyUserContext.new(integration_project_instance, 'tester', {}) + thread = user_context_obj.fetch_qualified_segments do |success| + expect(success).to be true + expect(user_context_obj.qualified_segments).to eq %w[great] + end + thread.join + integration_project_instance.close + end + + it 'should decide correctly with non-blocking' do + stub_request(:post, impression_log_url) + stub_request(:post, 'https://api.zaius.com/v3/graphql').to_return(status: 200, body: integrated_response_data.to_json) + stub_request(:post, 'https://api.zaius.com/v3/events').to_return(status: 200) + user_context_obj = Optimizely::OptimizelyUserContext.new(integration_project_instance, 'tester', {}) + thread = user_context_obj.fetch_qualified_segments do |success| + expect(success).to be true + decision = user_context_obj.decide('flag-segment') + expect(decision.variation_key).to eq 'variation-a' + end + thread.join + integration_project_instance.close + end end end diff --git a/spec/project_spec.rb b/spec/project_spec.rb index c9232099..f114447d 100644 --- a/spec/project_spec.rb +++ b/spec/project_spec.rb @@ -23,6 +23,7 @@ require 'optimizely/event/batch_event_processor' require 'optimizely/exceptions' require 'optimizely/helpers/validator' +require 'optimizely/helpers/sdk_settings' require 'optimizely/optimizely_user_context' require 'optimizely/version' @@ -31,6 +32,7 @@ let(:config_body_JSON) { OptimizelySpec::VALID_CONFIG_BODY_JSON } let(:config_body_invalid_JSON) { OptimizelySpec::INVALID_CONFIG_BODY_JSON } let(:config_body_integrations) { OptimizelySpec::CONFIG_DICT_WITH_INTEGRATIONS } + let(:config_body_integrations_JSON) { OptimizelySpec::CONFIG_DICT_WITH_INTEGRATIONS_JSON } let(:error_handler) { Optimizely::RaiseErrorHandler.new } let(:spy_logger) { spy('logger') } let(:version) { Optimizely::VERSION } @@ -40,6 +42,7 @@ let(:project_config) { project_instance.config_manager.config } let(:time_now) { Time.now } let(:post_headers) { {'Content-Type' => 'application/json'} } + after(:example) { project_instance.close } it 'has a version number' do expect(Optimizely::VERSION).not_to be_nil @@ -52,14 +55,15 @@ describe '.initialize' do it 'should take in a custom logger when instantiating Project class' do class CustomLogger # rubocop:disable Lint/ConstantDefinitionInBlock - def log(log_message) + def log(_level, log_message) log_message end end logger = CustomLogger.new instance_with_logger = Optimizely::Project.new(config_body_JSON, nil, logger) - expect(instance_with_logger.logger.log('test_message')).to eq('test_message') + expect(instance_with_logger.logger.log(Logger::INFO, 'test_message')).to eq('test_message') + instance_with_logger.close end it 'should take in a custom error handler when instantiating Project class' do @@ -72,51 +76,63 @@ def handle_error(error) error_handler = CustomErrorHandler.new instance_with_error_handler = Optimizely::Project.new(config_body_JSON, nil, nil, error_handler) expect(instance_with_error_handler.error_handler.handle_error('test_message')).to eq('test_message') + instance_with_error_handler.close end it 'should log an error when datafile is null' do expect_any_instance_of(Optimizely::SimpleLogger).to receive(:log).once.with(Logger::ERROR, 'Provided datafile is in an invalid format.') - Optimizely::Project.new(nil) + Optimizely::Project.new(nil).close end it 'should log an error when datafile is empty' do expect_any_instance_of(Optimizely::SimpleLogger).to receive(:log).once.with(Logger::ERROR, 'Provided datafile is in an invalid format.') - Optimizely::Project.new('') + Optimizely::Project.new('').close end it 'should log an error when given a datafile that does not conform to the schema' do + allow_any_instance_of(Optimizely::SimpleLogger).to receive(:log).with(Logger::INFO, anything) + allow_any_instance_of(Optimizely::SimpleLogger).to receive(:log).with(Logger::DEBUG, anything) expect_any_instance_of(Optimizely::SimpleLogger).to receive(:log).once.with(Logger::ERROR, 'Provided datafile is in an invalid format.') - Optimizely::Project.new('{"foo": "bar"}') + Optimizely::Project.new('{"foo": "bar"}').close end it 'should log an error when given an invalid logger' do + allow_any_instance_of(Optimizely::SimpleLogger).to receive(:log).with(Logger::DEBUG, anything) + allow_any_instance_of(Optimizely::SimpleLogger).to receive(:log).with(Logger::INFO, anything) expect_any_instance_of(Optimizely::SimpleLogger).to receive(:log).once.with(Logger::ERROR, 'Provided logger is in an invalid format.') class InvalidLogger; end # rubocop:disable Lint/ConstantDefinitionInBlock - Optimizely::Project.new(config_body_JSON, nil, InvalidLogger.new) + Optimizely::Project.new(config_body_JSON, nil, InvalidLogger.new).close end it 'should log an error when given an invalid event_dispatcher' do + allow_any_instance_of(Optimizely::SimpleLogger).to receive(:log).with(Logger::INFO, anything) + allow_any_instance_of(Optimizely::SimpleLogger).to receive(:log).with(Logger::DEBUG, anything) expect_any_instance_of(Optimizely::SimpleLogger).to receive(:log).once.with(Logger::ERROR, 'Provided event_dispatcher is in an invalid format.') class InvalidEventDispatcher; end # rubocop:disable Lint/ConstantDefinitionInBlock - Optimizely::Project.new(config_body_JSON, InvalidEventDispatcher.new) + Optimizely::Project.new(config_body_JSON, InvalidEventDispatcher.new).close end it 'should log an error when given an invalid error_handler' do + allow_any_instance_of(Optimizely::SimpleLogger).to receive(:log).with(Logger::INFO, anything) + allow_any_instance_of(Optimizely::SimpleLogger).to receive(:log).with(Logger::DEBUG, anything) expect_any_instance_of(Optimizely::SimpleLogger).to receive(:log).once.with(Logger::ERROR, 'Provided error_handler is in an invalid format.') class InvalidErrorHandler; end # rubocop:disable Lint/ConstantDefinitionInBlock - Optimizely::Project.new(config_body_JSON, nil, nil, InvalidErrorHandler.new) + Optimizely::Project.new(config_body_JSON, nil, nil, InvalidErrorHandler.new).close end it 'should not validate the JSON schema of the datafile when skip_json_validation is true' do + project_instance.close expect(Optimizely::Helpers::Validator).not_to receive(:datafile_valid?) - Optimizely::Project.new(config_body_JSON, nil, nil, nil, true) + Optimizely::Project.new(config_body_JSON, nil, nil, nil, true).close end it 'should be invalid when datafile contains integrations missing key' do + allow_any_instance_of(Optimizely::SimpleLogger).to receive(:log).with(Logger::INFO, anything) + allow_any_instance_of(Optimizely::SimpleLogger).to receive(:log).with(Logger::DEBUG, anything) expect_any_instance_of(Optimizely::SimpleLogger).to receive(:log).once.with(Logger::ERROR, 'Provided datafile is in an invalid format.') config = config_body_integrations.dup config['integrations'][0].delete('key') @@ -193,6 +209,14 @@ class InvalidErrorHandler; end # rubocop:disable Lint/ConstantDefinitionInBlock 'browser' => 'chrome' )).to be_instance_of(Optimizely::OptimizelyUserContext) end + + it 'should send identify event when called with odp enabled' do + project = Optimizely::Project.new(config_body_integrations_JSON, nil, spy_logger) + expect(project.odp_manager).to receive(:identify_user).with({user_id: 'tester'}) + project.create_user_context('tester') + + project.close + end end describe '#activate' do @@ -347,6 +371,10 @@ class InvalidErrorHandler; end # rubocop:disable Lint/ConstantDefinitionInBlock } end + after(:example) do + @project_typed_audience_instance.close + end + it 'should properly activate a user, (with attributes provided) when there is a typed audience with exact match type string' do params = @expected_activate_params @@ -807,6 +835,7 @@ def callback(_args); end invalid_project.activate('test_exp', 'test_user') expect(logger).to have_received(:log).with(Logger::ERROR, 'Provided datafile is in an invalid format.') expect(spy_logger).to have_received(:log).with(Logger::ERROR, "Optimizely instance is not valid. Failing 'activate'.") + invalid_project.close end it 'should return nil and log an error when Config Manager returns nil config' do @@ -876,7 +905,7 @@ def callback(_args); end notification_center: notification_center ) - project_instance = Optimizely::Project.new( + custom_project_instance = Optimizely::Project.new( nil, nil, spy_logger, error_handler, false, nil, nil, http_project_config_manager, notification_center ) @@ -884,7 +913,8 @@ def callback(_args); end sleep 0.1 until http_project_config_manager.ready? expect(http_project_config_manager.config).not_to eq(nil) - expect(project_instance.activate('test_experiment', 'test_user')).not_to eq(nil) + expect(custom_project_instance.activate('test_experiment', 'test_user')).not_to eq(nil) + custom_project_instance.close end it 'should update config, send update notification when sdk key is provided' do @@ -902,7 +932,7 @@ def callback(_args); end notification_center: notification_center ) - project_instance = Optimizely::Project.new( + custom_project_instance = Optimizely::Project.new( nil, nil, spy_logger, error_handler, false, nil, nil, http_project_config_manager, notification_center ) @@ -910,7 +940,8 @@ def callback(_args); end sleep 0.1 until http_project_config_manager.ready? expect(http_project_config_manager.config).not_to eq(nil) - expect(project_instance.activate('test_experiment', 'test_user')).not_to eq(nil) + expect(custom_project_instance.activate('test_experiment', 'test_user')).not_to eq(nil) + custom_project_instance.close end end @@ -935,15 +966,16 @@ def callback(_args); end expect(notification_center).to receive(:send_notifications).ordered expect(notification_center).to receive(:send_notifications).ordered - project_instance = Optimizely::Project.new( + custom_project_instance = Optimizely::Project.new( nil, nil, spy_logger, error_handler, false, nil, 'valid_sdk_key', nil, notification_center ) - sleep 0.1 until project_instance.config_manager.ready? + sleep 0.1 until custom_project_instance.config_manager.ready? - expect(project_instance.is_valid).to be true - expect(project_instance.activate('test_experiment', 'test_user')).not_to eq(nil) + expect(custom_project_instance.is_valid).to be true + expect(custom_project_instance.activate('test_experiment', 'test_user')).not_to eq(nil) + custom_project_instance.close end end end @@ -1022,15 +1054,16 @@ def callback(_args); end end it 'should properly track an event with tags even when the project does not have a custom logger' do - project_instance = Optimizely::Project.new(config_body_JSON) + custom_project_instance = Optimizely::Project.new(config_body_JSON) params = @expected_track_event_params params[:visitors][0][:snapshots][0][:events][0][:tags] = {revenue: 42} - project_instance.decision_service.set_forced_variation(project_config, 'test_experiment', 'test_user', 'variation') - allow(project_instance.event_dispatcher).to receive(:dispatch_event).with(instance_of(Optimizely::Event)) - project_instance.track('test_event', 'test_user', nil, revenue: 42) - expect(project_instance.event_dispatcher).to have_received(:dispatch_event).with(Optimizely::Event.new(:post, conversion_log_url, params, post_headers)).once + custom_project_instance.decision_service.set_forced_variation(project_config, 'test_experiment', 'test_user', 'variation') + allow(custom_project_instance.event_dispatcher).to receive(:dispatch_event).with(instance_of(Optimizely::Event)) + custom_project_instance.track('test_event', 'test_user', nil, revenue: 42) + expect(custom_project_instance.event_dispatcher).to have_received(:dispatch_event).with(Optimizely::Event.new(:post, conversion_log_url, params, post_headers)).once + custom_project_instance.close end it 'should log a message if an exception has occurred during dispatching of the event' do @@ -1127,6 +1160,9 @@ def callback(_args); end client_version: Optimizely::VERSION } end + after(:example) do + @project_typed_audience_instance.close + end it 'should call dispatch_event with right params when attributes are provided' do # Should be included via substring match string audience with id '3988293898' @@ -1277,6 +1313,7 @@ def callback(_args); end invalid_project.track('test_event', 'test_user') expect(logger).to have_received(:log).with(Logger::ERROR, 'Provided datafile is in an invalid format.') expect(spy_logger).to have_received(:log).with(Logger::ERROR, "Optimizely instance is not valid. Failing 'track'.") + invalid_project.close end it 'should return nil and log an error when Config Manager returns nil config' do @@ -1387,6 +1424,7 @@ def callback(_args); end invalid_project.get_variation('test_exp', 'test_user') expect(logger).to have_received(:log).with(Logger::ERROR, 'Provided datafile is in an invalid format.') expect(spy_logger).to have_received(:log).with(Logger::ERROR, "Optimizely instance is not valid. Failing 'get_variation'.") + invalid_project.close end it 'should return nil and log an error when Config Manager returns nil config' do @@ -1477,6 +1515,7 @@ def callback(_args); end expect(invalid_project.is_feature_enabled('totally_invalid_feature_key', 'test_user')).to be false expect(logger).to have_received(:log).once.with(Logger::ERROR, 'Provided datafile is in an invalid format.') expect(spy_logger).to have_received(:log).once.with(Logger::ERROR, "Optimizely instance is not valid. Failing 'is_feature_enabled'.") + invalid_project.close end it 'should return false when the feature flag key is nil' do @@ -1597,6 +1636,9 @@ def callback(_args); end @project_typed_audience_instance = Optimizely::Project.new(JSON.dump(OptimizelySpec::CONFIG_DICT_WITH_TYPED_AUDIENCES), nil, spy_logger, error_handler) stub_request(:post, impression_log_url) end + after(:example) do + @project_typed_audience_instance.close + end it 'should return true for feature rollout when typed audience matched' do # Should be included via exists match audience with id '3988293899' @@ -1835,6 +1877,7 @@ def callback(_args); end expect(invalid_project.get_enabled_features('test_user')).to be_empty expect(logger).to have_received(:log).once.with(Logger::ERROR, 'Provided datafile is in an invalid format.') expect(spy_logger).to have_received(:log).once.with(Logger::ERROR, "Optimizely instance is not valid. Failing 'get_enabled_features'.") + invalid_project.close end it 'should call inputs_valid? with the proper arguments in get_enabled_features' do @@ -2064,6 +2107,7 @@ def callback(_args); end .to eq(nil) expect(logger).to have_received(:log).once.with(Logger::ERROR, 'Provided datafile is in an invalid format.') expect(spy_logger).to have_received(:log).once.with(Logger::ERROR, "Optimizely instance is not valid. Failing 'get_feature_variable_string'.") + invalid_project.close end it 'should return nil and log an error when Config Manager returns nil config' do @@ -2148,7 +2192,7 @@ def callback(_args); end expect(project_instance.get_feature_variable_string('string_single_variable_feature', 'string_variable', user_id, user_attributes)) .to eq('cta_1') - expect(spy_logger).to have_received(:log).once + expect(spy_logger).not_to have_received(:log).with(Logger::ERROR, anything) expect(spy_logger).to have_received(:log).once .with( Logger::INFO, @@ -2164,7 +2208,7 @@ def callback(_args); end expect(project_instance.get_feature_variable_string('string_single_variable_feature', 'string_variable', user_id, user_attributes)) .to eq('wingardium leviosa') - expect(spy_logger).to have_received(:log).once + expect(spy_logger).not_to have_received(:log).with(Logger::ERROR, anything) expect(spy_logger).to have_received(:log).once .with( Logger::INFO, @@ -2177,7 +2221,6 @@ def callback(_args); end it 'should log an error message and return nil' do expect(project_instance.get_feature_variable_string('totally_invalid_feature_key', 'string_variable', user_id, user_attributes)) .to eq(nil) - expect(spy_logger).to have_received(:log).exactly(2).times expect(spy_logger).to have_received(:log).once .with( Logger::ERROR, @@ -2195,7 +2238,6 @@ def callback(_args); end it 'should log an error message and return nil' do expect(project_instance.get_feature_variable_string('string_single_variable_feature', 'invalid_string_variable', user_id, user_attributes)) .to eq(nil) - expect(spy_logger).to have_received(:log).once expect(spy_logger).to have_received(:log).once .with( Logger::ERROR, @@ -2218,6 +2260,7 @@ def callback(_args); end .to eq(nil) expect(logger).to have_received(:log).once.with(Logger::ERROR, 'Provided datafile is in an invalid format.') expect(spy_logger).to have_received(:log).once.with(Logger::ERROR, "Optimizely instance is not valid. Failing 'get_feature_variable_json'.") + invalid_project.close end it 'should return nil and log an error when Config Manager returns nil config' do @@ -2326,7 +2369,7 @@ def callback(_args); end expect(project_instance.get_feature_variable_json('json_single_variable_feature', 'json_variable', user_id, user_attributes)) .to eq('value' => 'cta_1') - expect(spy_logger).to have_received(:log).once + expect(spy_logger).not_to have_received(:log).with(Logger::ERROR, anything) expect(spy_logger).to have_received(:log).once .with( Logger::INFO, @@ -2354,7 +2397,7 @@ def callback(_args); end expect(project_instance.get_feature_variable_json('json_single_variable_feature', 'json_variable', user_id, user_attributes)) .to eq('val' => 'wingardium leviosa') - expect(spy_logger).to have_received(:log).once + expect(spy_logger).not_to have_received(:log).with(Logger::ERROR, anything) expect(spy_logger).to have_received(:log).once .with( Logger::INFO, @@ -2367,17 +2410,13 @@ def callback(_args); end it 'should log an error message and return nil' do expect(project_instance.get_feature_variable_json('totally_invalid_feature_key', 'json_variable', user_id, user_attributes)) .to eq(nil) - expect(spy_logger).to have_received(:log).twice expect(spy_logger).to have_received(:log).once .with( Logger::ERROR, "Feature flag key 'totally_invalid_feature_key' is not in datafile." ) - expect(spy_logger).to have_received(:log).once - .with( - Logger::INFO, - "No feature flag was found for key 'totally_invalid_feature_key'." - ) + expect(spy_logger).to have_received(:log) + .with(Logger::INFO, "No feature flag was found for key 'totally_invalid_feature_key'.") end end @@ -2385,7 +2424,6 @@ def callback(_args); end it 'should log an error message and return nil' do expect(project_instance.get_feature_variable_json('json_single_variable_feature', 'invalid_json_variable', user_id, user_attributes)) .to eq(nil) - expect(spy_logger).to have_received(:log).once expect(spy_logger).to have_received(:log).once .with( Logger::ERROR, @@ -2408,6 +2446,7 @@ def callback(_args); end .to eq(nil) expect(logger).to have_received(:log).once.with(Logger::ERROR, 'Provided datafile is in an invalid format.') expect(spy_logger).to have_received(:log).once.with(Logger::ERROR, "Optimizely instance is not valid. Failing 'get_feature_variable_boolean'.") + invalid_project.close end it 'should return nil and log an error when Config Manager returns nil config' do @@ -2432,8 +2471,7 @@ def callback(_args); end expect(project_instance.get_feature_variable_boolean('boolean_single_variable_feature', 'boolean_variable', user_id, user_attributes)) .to eq(true) - - expect(spy_logger).to have_received(:log).once + expect(spy_logger).not_to have_received(:log).with(Logger::ERROR, anything) expect(spy_logger).to have_received(:log).once .with( Logger::INFO, @@ -2455,6 +2493,7 @@ def callback(_args); end .to eq(nil) expect(logger).to have_received(:log).once.with(Logger::ERROR, 'Provided datafile is in an invalid format.') expect(spy_logger).to have_received(:log).once.with(Logger::ERROR, "Optimizely instance is not valid. Failing 'get_feature_variable_double'.") + invalid_project.close end it 'should return nil and log an error when Config Manager returns nil config' do @@ -2481,7 +2520,7 @@ def callback(_args); end expect(project_instance.get_feature_variable_double('double_single_variable_feature', 'double_variable', user_id, user_attributes)) .to eq(42.42) - expect(spy_logger).to have_received(:log).once + expect(spy_logger).not_to have_received(:log).with(Logger::ERROR, anything) expect(spy_logger).to have_received(:log).once .with( Logger::INFO, @@ -2503,6 +2542,7 @@ def callback(_args); end .to eq(nil) expect(logger).to have_received(:log).once.with(Logger::ERROR, 'Provided datafile is in an invalid format.') expect(spy_logger).to have_received(:log).once.with(Logger::ERROR, "Optimizely instance is not valid. Failing 'get_feature_variable_integer'.") + invalid_project.close end it 'should return nil and log an error when Config Manager returns nil config' do @@ -2529,7 +2569,7 @@ def callback(_args); end expect(project_instance.get_feature_variable_integer('integer_single_variable_feature', 'integer_variable', user_id, user_attributes)) .to eq(42) - expect(spy_logger).to have_received(:log).once + expect(spy_logger).not_to have_received(:log).with(Logger::ERROR, anything) expect(spy_logger).to have_received(:log).once .with( Logger::INFO, @@ -2551,6 +2591,7 @@ def callback(_args); end .to eq(nil) expect(logger).to have_received(:log).once.with(Logger::ERROR, 'Provided datafile is in an invalid format.') expect(spy_logger).to have_received(:log).once.with(Logger::ERROR, "Optimizely instance is not valid. Failing 'get_all_feature_variables'.") + invalid_project.close end it 'should return nil and log an error when Config Manager returns nil config' do @@ -2759,7 +2800,6 @@ def callback(_args); end it 'should log an error message and return nil' do expect(project_instance.get_all_feature_variables('totally_invalid_feature_key', user_id, user_attributes)) .to eq(nil) - expect(spy_logger).to have_received(:log).twice expect(spy_logger).to have_received(:log).once .with( Logger::ERROR, @@ -2787,6 +2827,7 @@ def callback(_args); end .to eq(nil) expect(logger).to have_received(:log).once.with(Logger::ERROR, 'Provided datafile is in an invalid format.') expect(spy_logger).to have_received(:log).once.with(Logger::ERROR, "Optimizely instance is not valid. Failing 'get_feature_variable'.") + invalid_project.close end it 'should return nil and log an error when Config Manager returns nil config' do @@ -2831,7 +2872,7 @@ def callback(_args); end expect(project_instance.get_feature_variable('string_single_variable_feature', 'string_variable', user_id, user_attributes)) .to eq('cta_1') - expect(spy_logger).to have_received(:log).once + expect(spy_logger).not_to have_received(:log).with(Logger::ERROR, anything) expect(spy_logger).to have_received(:log).once .with( Logger::INFO, @@ -2852,7 +2893,7 @@ def callback(_args); end expect(project_instance.get_feature_variable('boolean_single_variable_feature', 'boolean_variable', user_id, user_attributes)) .to eq(true) - expect(spy_logger).to have_received(:log).once + expect(spy_logger).not_to have_received(:log).with(Logger::ERROR, anything) expect(spy_logger).to have_received(:log).once .with( Logger::INFO, @@ -2874,7 +2915,7 @@ def callback(_args); end expect(project_instance.get_feature_variable('double_single_variable_feature', 'double_variable', user_id, user_attributes)) .to eq(42.42) - expect(spy_logger).to have_received(:log).once + expect(spy_logger).not_to have_received(:log).with(Logger::ERROR, anything) expect(spy_logger).to have_received(:log).once .with( Logger::INFO, @@ -2896,7 +2937,7 @@ def callback(_args); end expect(project_instance.get_feature_variable('integer_single_variable_feature', 'integer_variable', user_id, user_attributes)) .to eq(42) - expect(spy_logger).to have_received(:log).once + expect(spy_logger).not_to have_received(:log).with(Logger::ERROR, anything) expect(spy_logger).to have_received(:log).once .with( Logger::INFO, @@ -2912,7 +2953,7 @@ def callback(_args); end expect(project_instance.get_feature_variable('string_single_variable_feature', 'string_variable', user_id, user_attributes)) .to eq('wingardium leviosa') - expect(spy_logger).to have_received(:log).once + expect(spy_logger).not_to have_received(:log).with(Logger::ERROR, anything) expect(spy_logger).to have_received(:log).once .with( Logger::INFO, @@ -2925,7 +2966,6 @@ def callback(_args); end it 'should log an error message and return nil' do expect(project_instance.get_feature_variable('totally_invalid_feature_key', 'string_variable', user_id, user_attributes)) .to eq(nil) - expect(spy_logger).to have_received(:log).twice expect(spy_logger).to have_received(:log).once .with( Logger::ERROR, @@ -2943,7 +2983,6 @@ def callback(_args); end it 'should log an error message and return nil' do expect(project_instance.get_feature_variable('string_single_variable_feature', 'invalid_string_variable', user_id, user_attributes)) .to eq(nil) - expect(spy_logger).to have_received(:log).once expect(spy_logger).to have_received(:log).once .with( Logger::ERROR, @@ -2995,6 +3034,9 @@ def callback(_args); end before(:example) do @project_typed_audience_instance = Optimizely::Project.new(JSON.dump(OptimizelySpec::CONFIG_DICT_WITH_TYPED_AUDIENCES), nil, spy_logger, error_handler) end + after(:example) do + @project_typed_audience_instance.close + end it 'should return variable value when typed audience match' do # Should be included in the feature test via greater-than match audience with id '3468206647' @@ -3302,6 +3344,7 @@ def callback(_args); end invalid_project.set_forced_variation(valid_experiment[:key], user_id, valid_variation[:key]) expect(logger).to have_received(:log).with(Logger::ERROR, 'Provided datafile is in an invalid format.') expect(spy_logger).to have_received(:log).with(Logger::ERROR, "Optimizely instance is not valid. Failing 'set_forced_variation'.") + invalid_project.close end it 'should return nil and log an error when Config Manager returns nil config' do @@ -3361,6 +3404,7 @@ def callback(_args); end invalid_project.get_forced_variation(valid_experiment[:key], user_id) expect(logger).to have_received(:log).with(Logger::ERROR, 'Provided datafile is in an invalid format.') expect(spy_logger).to have_received(:log).with(Logger::ERROR, "Optimizely instance is not valid. Failing 'get_forced_variation'.") + invalid_project.close end it 'should return nil and log an error when Config Manager returns nil config' do @@ -3404,6 +3448,7 @@ def callback(_args); end it 'should return false when called with an invalid datafile' do invalid_project = Optimizely::Project.new('invalid', nil, spy_logger) expect(invalid_project.is_valid).to be false + invalid_project.close end end @@ -3427,7 +3472,7 @@ def callback(_args); end event_processor = Optimizely::BatchEventProcessor.new(event_dispatcher: Optimizely::EventDispatcher.new) - Optimizely::Project.new(config_body_JSON, nil, spy_logger, error_handler) + Optimizely::Project.new(config_body_JSON, nil, spy_logger, error_handler).close project_instance = Optimizely::Project.new(nil, nil, nil, nil, true, nil, nil, config_manager, nil, event_processor) @@ -3540,6 +3585,7 @@ def callback(_args); end variables: {}, variation_key: nil ) + invalid_project.close end it 'when flag key is invalid' do @@ -4043,6 +4089,7 @@ def callback(_args); end user_context = project_instance.create_user_context('user1') decisions = invalid_project.decide_all(user_context) expect(decisions).to eq({}) + invalid_project.close end it 'should get all the decisions' do @@ -4109,6 +4156,7 @@ def callback(_args); end user_context = project_instance.create_user_context('user1') decisions = invalid_project.decide_for_keys(user_context, keys) expect(decisions).to eq({}) + invalid_project.close end it 'should get all the decisions for keys' do @@ -4206,6 +4254,7 @@ def callback(_args); end variables: {'integer_variable' => 42}, variation_key: 'control' ) + custom_project_instance.close end end @@ -4233,6 +4282,7 @@ def callback(_args); end variables: {'first_letter' => 'F', 'rest_of_name' => 'red'}, variation_key: 'Fred' ) + custom_project_instance.close end it 'should exclude variables when the option is set in default_decide_options' do @@ -4260,6 +4310,7 @@ def callback(_args); end variables: {}, variation_key: 'Fred' ) + custom_project_instance.close end end @@ -4311,6 +4362,7 @@ def callback(_args); end variables: {'first_letter' => 'H', 'rest_of_name' => 'arry'}, variation_key: nil ) + custom_project_instance.close end it 'should not include reasons when the option is not set in default_decide_options' do @@ -4343,6 +4395,7 @@ def callback(_args); end variables: {'first_letter' => 'H', 'rest_of_name' => 'arry'}, variation_key: nil ) + custom_project_instance.close end end @@ -4360,6 +4413,7 @@ def callback(_args); end allow(custom_project_instance.decision_service).to receive(:get_variation_for_feature).and_return(decision_to_return) user_context = custom_project_instance.create_user_context('user1') custom_project_instance.decide(user_context, 'multi_variate_feature') + custom_project_instance.close end it 'should not send event when option is set in default_decide_options' do @@ -4378,7 +4432,230 @@ def callback(_args); end allow(custom_project_instance.decision_service).to receive(:get_variation_for_feature).and_return(decision_to_return) user_context = custom_project_instance.create_user_context('user1') custom_project_instance.decide(user_context, 'multi_variate_feature') + custom_project_instance.close + end + end + end + + describe 'sdk_settings' do + it 'should log info when disabled' do + project_instance.close + stub_request(:get, 'https://cdn.optimizely.com/datafiles/sdk-key.json') + .to_return(status: 200, body: config_body_integrations_JSON) + sdk_settings = Optimizely::Helpers::OptimizelySdkSettings.new(disable_odp: true) + project = Optimizely::Project.new(nil, nil, spy_logger, error_handler, false, nil, 'sdk-key', nil, nil, nil, [], sdk_settings) + expect(project.odp_manager.instance_variable_get('@event_manager')).to be_nil + expect(project.odp_manager.instance_variable_get('@segment_manager')).to be_nil + project.close + + expect(spy_logger).not_to have_received(:log).with(Logger::ERROR, anything) + expect(spy_logger).to have_received(:log).once.with(Logger::INFO, 'ODP is not enabled.') + end + + it 'should accept cache_size' do + stub_request(:get, 'https://cdn.optimizely.com/datafiles/sdk-key.json') + .to_return(status: 200, body: config_body_integrations_JSON) + + sdk_settings = Optimizely::Helpers::OptimizelySdkSettings.new(segments_cache_size: 5) + project = Optimizely::Project.new(nil, nil, spy_logger, error_handler, false, nil, 'sdk-key', nil, nil, nil, [], sdk_settings) + segment_manager = project.odp_manager.instance_variable_get('@segment_manager') + expect(segment_manager.instance_variable_get('@segments_cache').capacity).to eq 5 + project.close + + expect(spy_logger).not_to have_received(:log).with(Logger::ERROR, anything) + end + + it 'should accept cache_timeout' do + stub_request(:get, 'https://cdn.optimizely.com/datafiles/sdk-key.json') + .to_return(status: 200, body: config_body_integrations_JSON) + sdk_settings = Optimizely::Helpers::OptimizelySdkSettings.new(segments_cache_timeout_in_secs: 5) + project = Optimizely::Project.new(nil, nil, spy_logger, error_handler, false, nil, 'sdk-key', nil, nil, nil, [], sdk_settings) + segment_manager = project.odp_manager.instance_variable_get('@segment_manager') + expect(segment_manager.instance_variable_get('@segments_cache').timeout).to eq 5 + project.close + + expect(spy_logger).not_to have_received(:log).with(Logger::ERROR, anything) + end + + it 'should accept cache_size and cache_timeout' do + stub_request(:get, 'https://cdn.optimizely.com/datafiles/sdk-key.json') + .to_return(status: 200, body: config_body_integrations_JSON) + sdk_settings = Optimizely::Helpers::OptimizelySdkSettings.new(segments_cache_size: 10, segments_cache_timeout_in_secs: 5) + project = Optimizely::Project.new(nil, nil, spy_logger, error_handler, false, nil, 'sdk-key', nil, nil, nil, [], sdk_settings) + segment_manager = project.odp_manager.instance_variable_get('@segment_manager') + segments_cache = segment_manager.instance_variable_get('@segments_cache') + expect(segments_cache.capacity).to eq 10 + expect(segments_cache.timeout).to eq 5 + project.close + + expect(spy_logger).not_to have_received(:log).with(Logger::ERROR, anything) + end + + it 'should accept valid custom cache' do + class CustomCache # rubocop:disable Lint/ConstantDefinitionInBlock + def reset; end + def lookup(key); end + def save(key, value); end end + + stub_request(:get, 'https://cdn.optimizely.com/datafiles/sdk-key.json') + .to_return(status: 200, body: config_body_integrations_JSON) + sdk_settings = Optimizely::Helpers::OptimizelySdkSettings.new(odp_segments_cache: CustomCache.new) + project = Optimizely::Project.new(nil, nil, spy_logger, error_handler, false, nil, 'sdk-key', nil, nil, nil, [], sdk_settings) + segment_manager = project.odp_manager.instance_variable_get('@segment_manager') + expect(segment_manager.instance_variable_get('@segments_cache')).to be_a CustomCache + project.close + + expect(spy_logger).not_to have_received(:log).with(Logger::ERROR, anything) + end + + it 'should revert to default cache when custom cache is invalid' do + class InvalidCustomCache; end # rubocop:disable Lint/ConstantDefinitionInBlock + + stub_request(:get, 'https://cdn.optimizely.com/datafiles/sdk-key.json') + .to_return(status: 200, body: config_body_integrations_JSON) + sdk_settings = Optimizely::Helpers::OptimizelySdkSettings.new(odp_segments_cache: InvalidCustomCache.new) + project = Optimizely::Project.new(nil, nil, spy_logger, error_handler, false, nil, 'sdk-key', nil, nil, nil, [], sdk_settings) + + segment_manager = project.odp_manager.instance_variable_get('@segment_manager') + expect(segment_manager.instance_variable_get('@segments_cache')).to be_a Optimizely::LRUCache + project.close + + expect(spy_logger).to have_received(:log).once.with(Logger::ERROR, 'Invalid ODP segments cache, reverting to default.') + end + + it 'should accept valid custom segment manager' do + class CustomSegmentManager # rubocop:disable Lint/ConstantDefinitionInBlock + attr_accessor :odp_config + + def initialize + @odp_config = nil + end + + def reset; end + def fetch_qualified_segments(user_key, user_value, options); end + end + + stub_request(:get, 'https://cdn.optimizely.com/datafiles/sdk-key.json') + .to_return(status: 200, body: config_body_integrations_JSON) + sdk_settings = Optimizely::Helpers::OptimizelySdkSettings.new(odp_segment_manager: CustomSegmentManager.new) + project = Optimizely::Project.new(config_body_integrations_JSON, nil, spy_logger, error_handler, false, nil, nil, nil, nil, nil, [], sdk_settings) + segment_manager = project.odp_manager.instance_variable_get('@segment_manager') + expect(segment_manager).to be_a CustomSegmentManager + project.fetch_qualified_segments(user_id: 'test') + project.close + + expect(spy_logger).not_to have_received(:log).with(Logger::ERROR, anything) + expect(spy_logger).to have_received(:log).once.with(Logger::INFO, 'Stopping ODP event queue.') + end + + it 'should revert to default segment manager when custom manager is invalid' do + class InvalidSegmentManager; end # rubocop:disable Lint/ConstantDefinitionInBlock + + stub_request(:get, 'https://cdn.optimizely.com/datafiles/sdk-key.json') + .to_return(status: 200, body: config_body_integrations_JSON) + sdk_settings = Optimizely::Helpers::OptimizelySdkSettings.new(odp_segment_manager: InvalidSegmentManager.new) + project = Optimizely::Project.new(nil, nil, spy_logger, error_handler, false, nil, 'sdk-key', nil, nil, nil, [], sdk_settings) + + segment_manager = project.odp_manager.instance_variable_get('@segment_manager') + expect(segment_manager).to be_a Optimizely::OdpSegmentManager + project.close + + expect(spy_logger).to have_received(:log).once.with(Logger::ERROR, 'Invalid ODP segment manager, reverting to default.') + end + + it 'should accept valid custom event manager' do + class CustomEventManager # rubocop:disable Lint/ConstantDefinitionInBlock + def send_event(extra_param = nil, action:, type:, identifiers:, data:, other_extra_param: 'great'); end + def start!(odp_config); end + def update_config; end + def stop!; end + end + + stub_request(:get, 'https://cdn.optimizely.com/datafiles/sdk-key.json') + .to_return(status: 200, body: config_body_integrations_JSON) + sdk_settings = Optimizely::Helpers::OptimizelySdkSettings.new(odp_event_manager: CustomEventManager.new) + project = Optimizely::Project.new(nil, nil, spy_logger, error_handler, false, nil, 'sdk-key', nil, nil, nil, [], sdk_settings) + event_manager = project.odp_manager.instance_variable_get('@event_manager') + expect(event_manager).to be_a CustomEventManager + project.send_odp_event(action: 'test') + project.close + + expect(spy_logger).not_to have_received(:log).with(Logger::ERROR, anything) + end + + it 'should revert to default event manager when custom manager is invalid' do + class InvalidEventManager; end # rubocop:disable Lint/ConstantDefinitionInBlock + + stub_request(:get, 'https://cdn.optimizely.com/datafiles/sdk-key.json') + .to_return(status: 200, body: config_body_integrations_JSON) + sdk_settings = Optimizely::Helpers::OptimizelySdkSettings.new(odp_event_manager: InvalidEventManager.new) + project = Optimizely::Project.new(nil, nil, spy_logger, error_handler, false, nil, 'sdk-key', nil, nil, nil, [], sdk_settings) + + event_manager = project.odp_manager.instance_variable_get('@event_manager') + expect(event_manager).to be_a Optimizely::OdpEventManager + project.close + + expect(spy_logger).to have_received(:log).once.with(Logger::ERROR, 'Invalid ODP event manager, reverting to default.') + end + end + + describe '#send_odp_event' do + it 'should send event with StaticProjectConfigManager' do + stub_request(:post, 'https://api.zaius.com/v3/events').to_return(status: 200) + expect(spy_logger).to receive(:log).once.with(Logger::DEBUG, 'ODP event queue: flushing batch size 1.') + expect(spy_logger).not_to receive(:log).with(Logger::ERROR, anything) + project = Optimizely::Project.new(config_body_integrations_JSON, nil, spy_logger) + project.send_odp_event(type: 'wow', action: 'great', identifiers: {}, data: {}) + project.close + end + + it 'should send event with HTTPProjectConfigManager' do + stub_request(:get, 'https://cdn.optimizely.com/datafiles/sdk-key.json') + .to_return(status: 200, body: config_body_integrations_JSON) + stub_request(:post, 'https://api.zaius.com/v3/events').to_return(status: 200) + expect(spy_logger).to receive(:log).once.with(Logger::DEBUG, 'ODP event queue: flushing batch size 1.') + expect(spy_logger).not_to receive(:log).with(Logger::ERROR, anything) + project = Optimizely::Project.new(nil, nil, spy_logger, nil, false, nil, 'sdk-key') + + # wait until project_config ready + project.send(:project_config) + + project.send_odp_event(type: 'wow', action: 'great', identifiers: {}, data: {}) + project.close + end + + it 'should log error when odp disabled' do + expect(spy_logger).to receive(:log).once.with(Logger::ERROR, 'ODP is not enabled.') + sdk_settings = Optimizely::Helpers::OptimizelySdkSettings.new(disable_odp: true) + custom_project_instance = Optimizely::Project.new(config_body_integrations_JSON, nil, spy_logger, error_handler, false, nil, nil, nil, nil, nil, [], sdk_settings) + custom_project_instance.send_odp_event(type: 'wow', action: 'great', identifiers: {}, data: {}) + custom_project_instance.close + end + + it 'should log debug if datafile not ready' do + expect(spy_logger).to receive(:log).once.with(Logger::DEBUG, 'ODP event queue: cannot send before config has been set.') + project = Optimizely::Project.new(nil, nil, spy_logger, nil, false, nil, 'sdk-key') + project.send_odp_event(type: 'wow', action: 'great', identifiers: {}, data: {}) + project.close + end + + it 'should log error if odp not enabled with HTTPProjectConfigManager' do + stub_request(:get, 'https://cdn.optimizely.com/datafiles/sdk-key.json') + .to_return(status: 200, body: config_body_integrations_JSON) + expect(spy_logger).to receive(:log).once.with(Logger::ERROR, 'ODP is not enabled.') + sdk_settings = Optimizely::Helpers::OptimizelySdkSettings.new(disable_odp: true) + project = Optimizely::Project.new(nil, nil, spy_logger, error_handler, false, nil, 'sdk-key', nil, nil, nil, [], sdk_settings) + sleep 0.1 until project.config_manager.ready? + project.send_odp_event(type: 'wow', action: 'great', identifiers: {}, data: {}) + project.close + end + + it 'should log error with invalid data' do + expect(spy_logger).to receive(:log).once.with(Logger::ERROR, 'ODP data is not valid.') + project = Optimizely::Project.new(config_body_integrations_JSON, nil, spy_logger) + project.send_odp_event(type: 'wow', action: 'great', identifiers: {}, data: {'wow': {}}) + project.close end end end diff --git a/spec/spec_params.rb b/spec/spec_params.rb index 62c585a9..1ebb09b7 100644 --- a/spec/spec_params.rb +++ b/spec/spec_params.rb @@ -1240,7 +1240,7 @@ module OptimizelySpec 'integrations' => [ { 'key' => 'odp', - 'host' => 'https =>//api.zaius.com', + 'host' => 'https://api.zaius.com', 'publicKey' => 'W4WzcEs-ABgXorzY7h1LCQ' } ], diff --git a/spec/user_condition_evaluator_spec.rb b/spec/user_condition_evaluator_spec.rb index d928cce3..a25cc0fc 100644 --- a/spec/user_condition_evaluator_spec.rb +++ b/spec/user_condition_evaluator_spec.rb @@ -27,6 +27,7 @@ let(:spy_logger) { spy('logger') } let(:project_instance) { Optimizely::Project.new(config_body_JSON, nil, spy_logger, error_handler) } let(:user_context) { project_instance.create_user_context('some-user', {}) } + after(:example) { project_instance.close } it 'should return true when the attributes pass the audience conditions and no match type is provided' do user_context.instance_variable_set(:@user_attributes, 'browser_type' => 'safari') @@ -61,7 +62,7 @@ user_context.instance_variable_set(:@user_attributes, 'weird_condition' => 'bye') condition_evaluator = Optimizely::UserConditionEvaluator.new(user_context, spy_logger) expect(condition_evaluator.evaluate(condition)).to eq(nil) - expect(spy_logger).to have_received(:log).exactly(1).times + expect(spy_logger).not_to have_received(:log).with(Logger::ERROR, anything) expect(spy_logger).to have_received(:log).once.with( Logger::WARN, "Audience condition #{condition} uses an unknown condition type. You may need to upgrade to a newer release of " \ @@ -74,7 +75,7 @@ user_context.instance_variable_set(:@user_attributes, 'weird_condition' => 'bye') condition_evaluator = Optimizely::UserConditionEvaluator.new(user_context, spy_logger) expect(condition_evaluator.evaluate(condition)).to eq(nil) - expect(spy_logger).to have_received(:log).exactly(1).times + expect(spy_logger).not_to have_received(:log).with(Logger::ERROR, anything) expect(spy_logger).to have_received(:log).once.with( Logger::WARN, "Audience condition #{condition} uses an unknown condition type. You may need to upgrade to a newer release of " \ @@ -102,7 +103,8 @@ it 'should return false if there is no user-provided value' do condition_evaluator = Optimizely::UserConditionEvaluator.new(user_context, spy_logger) expect(condition_evaluator.evaluate(@exists_conditions)).to be false - expect(spy_logger).not_to have_received(:log) + expect(spy_logger).not_to have_received(:log).with(Logger::ERROR, anything) + expect(spy_logger).not_to have_received(:log).with(Logger::WARN, anything) end it 'should return false if the user-provided value is nil' do