optimizely-sdk 4.0.1 → 5.0.0.pre.beta

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright 2022, Optimizely and contributors
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ require 'json'
20
+ require_relative '../exceptions'
21
+
22
+ module Optimizely
23
+ class OdpSegmentApiManager
24
+ # Interface that handles fetching audience segments.
25
+
26
+ def initialize(logger: nil, proxy_config: nil, timeout: nil)
27
+ @logger = logger || NoOpLogger.new
28
+ @proxy_config = proxy_config
29
+ @timeout = timeout || Optimizely::Helpers::Constants::ODP_GRAPHQL_API_CONFIG[:REQUEST_TIMEOUT]
30
+ end
31
+
32
+ # Fetch segments from the ODP GraphQL API.
33
+ #
34
+ # @param api_key - public api key
35
+ # @param api_host - domain url of the host
36
+ # @param user_key - vuid or fs_user_id (client device id or fullstack id)
37
+ # @param user_value - value of user_key
38
+ # @param segments_to_check - array of segments to check
39
+
40
+ def fetch_segments(api_key, api_host, user_key, user_value, segments_to_check)
41
+ url = "#{api_host}/v3/graphql"
42
+
43
+ headers = {'Content-Type' => 'application/json', 'x-api-key' => api_key.to_s}
44
+
45
+ payload = {
46
+ query: 'query($userId: String, $audiences: [String]) {' \
47
+ "customer(#{user_key}: $userId) " \
48
+ '{audiences(subset: $audiences) {edges {node {name state}}}}}',
49
+ variables: {
50
+ userId: user_value.to_s,
51
+ audiences: segments_to_check || []
52
+ }
53
+ }.to_json
54
+
55
+ begin
56
+ response = Helpers::HttpUtils.make_request(
57
+ url, :post, payload, headers, @timeout, @proxy_config
58
+ )
59
+ rescue SocketError, Timeout::Error, Net::ProtocolError, Errno::ECONNRESET => e
60
+ @logger.log(Logger::DEBUG, "GraphQL download failed: #{e}")
61
+ log_segments_failure('network error')
62
+ return nil
63
+ rescue Errno::EINVAL, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, HTTPUriError => e
64
+ log_segments_failure(e)
65
+ return nil
66
+ end
67
+
68
+ status = response.code.to_i
69
+ if status >= 400
70
+ log_segments_failure(status)
71
+ return nil
72
+ end
73
+
74
+ begin
75
+ response = JSON.parse(response.body)
76
+ rescue JSON::ParserError
77
+ log_segments_failure('JSON decode error')
78
+ return nil
79
+ end
80
+
81
+ if response.include?('errors')
82
+ error = response['errors'].first if response['errors'].is_a? Array
83
+ error_code = extract_component(error, 'extensions', 'code')
84
+ if error_code == 'INVALID_IDENTIFIER_EXCEPTION'
85
+ log_segments_failure('invalid identifier', Logger::WARN)
86
+ else
87
+ error_class = extract_component(error, 'extensions', 'classification') || 'decode error'
88
+ log_segments_failure(error_class)
89
+ end
90
+ return nil
91
+ end
92
+
93
+ audiences = extract_component(response, 'data', 'customer', 'audiences', 'edges')
94
+ unless audiences
95
+ log_segments_failure('decode error')
96
+ return nil
97
+ end
98
+
99
+ audiences.filter_map do |edge|
100
+ name = extract_component(edge, 'node', 'name')
101
+ state = extract_component(edge, 'node', 'state')
102
+ unless name && state
103
+ log_segments_failure('decode error')
104
+ return nil
105
+ end
106
+ state == 'qualified' ? name : nil
107
+ end
108
+ end
109
+
110
+ private
111
+
112
+ def log_segments_failure(message, level = Logger::ERROR)
113
+ @logger.log(level, format(Optimizely::Helpers::Constants::ODP_LOGS[:FETCH_SEGMENTS_FAILED], message))
114
+ end
115
+
116
+ def extract_component(hash, *components)
117
+ hash.dig(*components) if hash.is_a? Hash
118
+ rescue TypeError
119
+ nil
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright 2022, Optimizely and contributors
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ require 'optimizely/logger'
20
+ require_relative 'odp_segment_api_manager'
21
+
22
+ module Optimizely
23
+ class OdpSegmentManager
24
+ # Schedules connections to ODP for audience segmentation and caches the results
25
+ attr_accessor :odp_config
26
+ attr_reader :segments_cache, :api_manager, :logger
27
+
28
+ def initialize(segments_cache, api_manager = nil, logger = nil, proxy_config = nil, timeout: nil)
29
+ @odp_config = nil
30
+ @logger = logger || NoOpLogger.new
31
+ @api_manager = api_manager || OdpSegmentApiManager.new(logger: @logger, proxy_config: proxy_config, timeout: timeout)
32
+ @segments_cache = segments_cache
33
+ end
34
+
35
+ # Returns qualified segments for the user from the cache or the ODP server if not in the cache.
36
+ #
37
+ # @param user_key - The key for identifying the id type.
38
+ # @param user_value - The id itself.
39
+ # @param options - An array of OptimizelySegmentOptions used to ignore and/or reset the cache.
40
+ #
41
+ # @return - Array of qualified segments.
42
+ def fetch_qualified_segments(user_key, user_value, options)
43
+ odp_api_key = @odp_config&.api_key
44
+ odp_api_host = @odp_config&.api_host
45
+ segments_to_check = @odp_config&.segments_to_check
46
+
47
+ if odp_api_key.nil? || odp_api_host.nil?
48
+ @logger.log(Logger::ERROR, format(Optimizely::Helpers::Constants::ODP_LOGS[:FETCH_SEGMENTS_FAILED], 'ODP is not enabled'))
49
+ return nil
50
+ end
51
+
52
+ unless segments_to_check&.size&.positive?
53
+ @logger.log(Logger::DEBUG, 'No segments are used in the project. Returning empty list')
54
+ return []
55
+ end
56
+
57
+ cache_key = make_cache_key(user_key, user_value)
58
+
59
+ ignore_cache = options.include?(OptimizelySegmentOption::IGNORE_CACHE)
60
+ reset_cache = options.include?(OptimizelySegmentOption::RESET_CACHE)
61
+
62
+ reset if reset_cache
63
+
64
+ unless ignore_cache || reset_cache
65
+ segments = @segments_cache.lookup(cache_key)
66
+ unless segments.nil?
67
+ @logger.log(Logger::DEBUG, 'ODP cache hit. Returning segments from cache.')
68
+ return segments
69
+ end
70
+ @logger.log(Logger::DEBUG, 'ODP cache miss.')
71
+ end
72
+
73
+ @logger.log(Logger::DEBUG, 'Making a call to ODP server.')
74
+
75
+ segments = @api_manager.fetch_segments(odp_api_key, odp_api_host, user_key, user_value, segments_to_check)
76
+ @segments_cache.save(cache_key, segments) unless segments.nil? || ignore_cache
77
+ segments
78
+ end
79
+
80
+ def reset
81
+ @segments_cache.reset
82
+ nil
83
+ end
84
+
85
+ private
86
+
87
+ def make_cache_key(user_key, user_value)
88
+ "#{user_key}-$-#{user_value}"
89
+ end
90
+ end
91
+
92
+ class OptimizelySegmentOption
93
+ # Options for the OdpSegmentManager
94
+ IGNORE_CACHE = :IGNORE_CACHE
95
+ RESET_CACHE = :RESET_CACHE
96
+ end
97
+ end
@@ -201,7 +201,7 @@ module Optimizely
201
201
  def stringify_conditions(conditions, audiences_map)
202
202
  operand = 'OR'
203
203
  conditions_str = ''
204
- length = conditions.length()
204
+ length = conditions.length
205
205
  return '' if length.zero?
206
206
  return "\"#{lookup_name_from_id(conditions[0], audiences_map)}\"" if length == 1 && !OPERATORS.include?(conditions[0])
207
207
 
@@ -126,6 +126,7 @@ module Optimizely
126
126
  # @param user_profile_service - Optional UserProfileServiceInterface Provides methods to store and retreive user profiles.
127
127
  # @param config_manager - Optional ConfigManagerInterface Responds to 'config' method.
128
128
  # @param notification_center - Optional Instance of NotificationCenter.
129
+ # @param settings: Optional instance of OptimizelySdkSettings for sdk configuration.
129
130
  #
130
131
  # if @max_event_batch_size and @max_event_flush_interval are nil then default batchsize and flush_interval
131
132
  # will be used to setup batchEventProcessor.
@@ -138,7 +139,8 @@ module Optimizely
138
139
  skip_json_validation = false, # rubocop:disable Style/OptionalBooleanParameter
139
140
  user_profile_service = nil,
140
141
  config_manager = nil,
141
- notification_center = nil
142
+ notification_center = nil,
143
+ settings = nil
142
144
  )
143
145
 
144
146
  error_handler ||= NoOpErrorHandler.new
@@ -174,7 +176,10 @@ module Optimizely
174
176
  sdk_key,
175
177
  config_manager,
176
178
  notification_center,
177
- event_processor
179
+ event_processor,
180
+ [],
181
+ {},
182
+ settings
178
183
  )
179
184
  end
180
185
  end
@@ -26,7 +26,7 @@ module Optimizely
26
26
 
27
27
  OptimizelyDecisionContext = Struct.new(:flag_key, :rule_key)
28
28
  OptimizelyForcedDecision = Struct.new(:variation_key)
29
- def initialize(optimizely_client, user_id, user_attributes)
29
+ def initialize(optimizely_client, user_id, user_attributes, identify: true)
30
30
  @attr_mutex = Mutex.new
31
31
  @forced_decision_mutex = Mutex.new
32
32
  @qualified_segment_mutex = Mutex.new
@@ -34,13 +34,15 @@ module Optimizely
34
34
  @user_id = user_id
35
35
  @user_attributes = user_attributes.nil? ? {} : user_attributes.clone
36
36
  @forced_decisions = {}
37
- @qualified_segments = []
37
+ @qualified_segments = nil
38
+
39
+ @optimizely_client&.identify_user(user_id: user_id) if identify
38
40
  end
39
41
 
40
42
  def clone
41
- user_context = OptimizelyUserContext.new(@optimizely_client, @user_id, user_attributes)
43
+ user_context = OptimizelyUserContext.new(@optimizely_client, @user_id, user_attributes, identify: false)
42
44
  @forced_decision_mutex.synchronize { user_context.instance_variable_set('@forced_decisions', @forced_decisions.dup) unless @forced_decisions.empty? }
43
- @qualified_segment_mutex.synchronize { user_context.instance_variable_set('@qualified_segments', @qualified_segments.dup) unless @qualified_segments.empty? }
45
+ @qualified_segment_mutex.synchronize { user_context.instance_variable_set('@qualified_segments', @qualified_segments.dup) unless @qualified_segments.nil? }
44
46
  user_context
45
47
  end
46
48
 
@@ -194,11 +196,43 @@ module Optimizely
194
196
  # Checks if user is qualified for the provided segment.
195
197
  #
196
198
  # @param segment - A segment name
199
+ # @return true if qualified.
197
200
 
198
201
  def qualified_for?(segment)
199
- return false if @qualified_segments.empty?
202
+ qualified = false
203
+ @qualified_segment_mutex.synchronize do
204
+ break if @qualified_segments.nil? || @qualified_segments.empty?
205
+
206
+ qualified = @qualified_segments.include?(segment)
207
+ end
208
+ qualified
209
+ end
210
+
211
+ # Fetch all qualified segments for the user context.
212
+ #
213
+ # The segments fetched will be saved in `@qualified_segments` and can be accessed any time.
214
+ #
215
+ # @param options - A set of options for fetching qualified segments (optional).
216
+ # @param block - An optional block to call after segments have been fetched.
217
+ # If a block is provided, segments will be fetched on a separate thread.
218
+ # Block will be called with a boolean indicating if the fetch succeeded.
219
+ # @return If no block is provided, a boolean indicating whether the fetch was successful.
220
+ # Otherwise, returns a thread handle and the status boolean is passed to the block.
200
221
 
201
- @qualified_segment_mutex.synchronize { @qualified_segments.include?(segment) }
222
+ def fetch_qualified_segments(options: [], &block)
223
+ fetch_segments = lambda do |opts, callback|
224
+ segments = @optimizely_client&.fetch_qualified_segments(user_id: @user_id, options: opts)
225
+ self.qualified_segments = segments
226
+ success = !segments.nil?
227
+ callback&.call(success)
228
+ success
229
+ end
230
+
231
+ if block_given?
232
+ Thread.new(options, block, &fetch_segments)
233
+ else
234
+ fetch_segments.call(options, nil)
235
+ end
202
236
  end
203
237
  end
204
238
  end
@@ -331,7 +331,7 @@ module Optimizely
331
331
  end
332
332
 
333
333
  def qualified_evaluator(condition)
334
- # Evaluate the given match condition for the given user qaulified segments.
334
+ # Evaluate the given match condition for the given user qualified segments.
335
335
  # Returns boolean true if condition value is in the user's qualified segments,
336
336
  # false if the condition value is not in the user's qualified segments,
337
337
  # nil if the condition value isn't a string.
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  #
4
- # Copyright 2016-2019, Optimizely and contributors
4
+ # Copyright 2016-2023, Optimizely and contributors
5
5
  #
6
6
  # Licensed under the Apache License, Version 2.0 (the "License");
7
7
  # you may not use this file except in compliance with the License.
@@ -17,5 +17,5 @@
17
17
  #
18
18
  module Optimizely
19
19
  CLIENT_ENGINE = 'ruby-sdk'
20
- VERSION = '4.0.1'
20
+ VERSION = '5.0.0-beta'
21
21
  end
data/lib/optimizely.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  #
4
- # Copyright 2016-2022, Optimizely and contributors
4
+ # Copyright 2016-2023, Optimizely and contributors
5
5
  #
6
6
  # Licensed under the Apache License, Version 2.0 (the "License");
7
7
  # you may not use this file except in compliance with the License.
@@ -25,7 +25,7 @@ require_relative 'optimizely/decide/optimizely_decision_message'
25
25
  require_relative 'optimizely/decision_service'
26
26
  require_relative 'optimizely/error_handler'
27
27
  require_relative 'optimizely/event_builder'
28
- require_relative 'optimizely/event/forwarding_event_processor'
28
+ require_relative 'optimizely/event/batch_event_processor'
29
29
  require_relative 'optimizely/event/event_factory'
30
30
  require_relative 'optimizely/event/user_event_factory'
31
31
  require_relative 'optimizely/event_dispatcher'
@@ -36,8 +36,12 @@ require_relative 'optimizely/helpers/validator'
36
36
  require_relative 'optimizely/helpers/variable_type'
37
37
  require_relative 'optimizely/logger'
38
38
  require_relative 'optimizely/notification_center'
39
+ require_relative 'optimizely/notification_center_registry'
39
40
  require_relative 'optimizely/optimizely_config'
40
41
  require_relative 'optimizely/optimizely_user_context'
42
+ require_relative 'optimizely/odp/lru_cache'
43
+ require_relative 'optimizely/odp/odp_manager'
44
+ require_relative 'optimizely/helpers/sdk_settings'
41
45
 
42
46
  module Optimizely
43
47
  class Project
@@ -46,7 +50,7 @@ module Optimizely
46
50
  attr_reader :notification_center
47
51
  # @api no-doc
48
52
  attr_reader :config_manager, :decision_service, :error_handler, :event_dispatcher,
49
- :event_processor, :logger, :stopped
53
+ :event_processor, :logger, :odp_manager, :stopped
50
54
 
51
55
  # Constructor for Projects.
52
56
  #
@@ -62,6 +66,9 @@ module Optimizely
62
66
  # @param config_manager - Optional Responds to 'config' method.
63
67
  # @param notification_center - Optional Instance of NotificationCenter.
64
68
  # @param event_processor - Optional Responds to process.
69
+ # @param default_decide_options: Optional default decision options.
70
+ # @param event_processor_options: Optional hash of options to be passed to the default batch event processor.
71
+ # @param settings: Optional instance of OptimizelySdkSettings for sdk configuration.
65
72
 
66
73
  def initialize( # rubocop:disable Metrics/ParameterLists
67
74
  datafile = nil,
@@ -74,13 +81,16 @@ module Optimizely
74
81
  config_manager = nil,
75
82
  notification_center = nil,
76
83
  event_processor = nil,
77
- default_decide_options = []
84
+ default_decide_options = [],
85
+ event_processor_options = {},
86
+ settings = nil
78
87
  )
79
88
  @logger = logger || NoOpLogger.new
80
89
  @error_handler = error_handler || NoOpErrorHandler.new
81
90
  @event_dispatcher = event_dispatcher || EventDispatcher.new(logger: @logger, error_handler: @error_handler)
82
91
  @user_profile_service = user_profile_service
83
92
  @default_decide_options = []
93
+ @sdk_settings = settings
84
94
 
85
95
  if default_decide_options.is_a? Array
86
96
  @default_decide_options = default_decide_options.clone
@@ -89,6 +99,11 @@ module Optimizely
89
99
  @default_decide_options = []
90
100
  end
91
101
 
102
+ unless event_processor_options.is_a? Hash
103
+ @logger.log(Logger::DEBUG, 'Provided event processor options is not a hash.')
104
+ event_processor_options = {}
105
+ end
106
+
92
107
  begin
93
108
  validate_instantiation_options
94
109
  rescue InvalidInputError => e
@@ -98,7 +113,7 @@ module Optimizely
98
113
 
99
114
  @notification_center = notification_center.is_a?(Optimizely::NotificationCenter) ? notification_center : NotificationCenter.new(@logger, @error_handler)
100
115
 
101
- @config_manager = if config_manager.respond_to?(:config)
116
+ @config_manager = if config_manager.respond_to?(:config) && config_manager.respond_to?(:sdk_key)
102
117
  config_manager
103
118
  elsif sdk_key
104
119
  HTTPProjectConfigManager.new(
@@ -113,12 +128,20 @@ module Optimizely
113
128
  StaticProjectConfigManager.new(datafile, @logger, @error_handler, skip_json_validation)
114
129
  end
115
130
 
131
+ setup_odp!(@config_manager.sdk_key)
132
+
116
133
  @decision_service = DecisionService.new(@logger, @user_profile_service)
117
134
 
118
135
  @event_processor = if event_processor.respond_to?(:process)
119
136
  event_processor
120
137
  else
121
- ForwardingEventProcessor.new(@event_dispatcher, @logger, @notification_center)
138
+ BatchEventProcessor.new(
139
+ event_dispatcher: @event_dispatcher,
140
+ logger: @logger,
141
+ notification_center: @notification_center,
142
+ batch_size: event_processor_options[:batch_size] || BatchEventProcessor::DEFAULT_BATCH_SIZE,
143
+ flush_interval: event_processor_options[:flush_interval] || BatchEventProcessor::DEFAULT_BATCH_INTERVAL
144
+ )
122
145
  end
123
146
  end
124
147
 
@@ -507,7 +530,7 @@ module Optimizely
507
530
  return false
508
531
  end
509
532
 
510
- user_context = create_user_context(user_id, attributes)
533
+ user_context = OptimizelyUserContext.new(self, user_id, attributes, identify: false)
511
534
  decision, = @decision_service.get_variation_for_feature(config, feature_flag, user_context)
512
535
 
513
536
  feature_enabled = false
@@ -747,7 +770,7 @@ module Optimizely
747
770
  return nil
748
771
  end
749
772
 
750
- user_context = create_user_context(user_id, attributes)
773
+ user_context = OptimizelyUserContext.new(self, user_id, attributes, identify: false)
751
774
  decision, = @decision_service.get_variation_for_feature(config, feature_flag, user_context)
752
775
  variation = decision ? decision['variation'] : nil
753
776
  feature_enabled = variation ? variation['featureEnabled'] : false
@@ -816,6 +839,7 @@ module Optimizely
816
839
  @stopped = true
817
840
  @config_manager.stop! if @config_manager.respond_to?(:stop!)
818
841
  @event_processor.stop! if @event_processor.respond_to?(:stop!)
842
+ @odp_manager.stop!
819
843
  end
820
844
 
821
845
  def get_optimizely_config
@@ -869,6 +893,52 @@ module Optimizely
869
893
  end
870
894
  end
871
895
 
896
+ # Send an event to the ODP server.
897
+ #
898
+ # @param action - the event action name. Cannot be nil or empty string.
899
+ # @param identifiers - a hash for identifiers. The caller must provide at least one key-value pair.
900
+ # @param type - the event type (default = "fullstack").
901
+ # @param data - a hash for associated data. The default event data will be added to this data before sending to the ODP server.
902
+
903
+ def send_odp_event(action:, identifiers:, type: Helpers::Constants::ODP_MANAGER_CONFIG[:EVENT_TYPE], data: {})
904
+ unless identifiers.is_a?(Hash) && !identifiers.empty?
905
+ @logger.log(Logger::ERROR, 'ODP events must have at least one key-value pair in identifiers.')
906
+ return
907
+ end
908
+
909
+ unless is_valid
910
+ @logger.log(Logger::ERROR, InvalidProjectConfigError.new('send_odp_event').message)
911
+ return
912
+ end
913
+
914
+ if action.nil? || action.empty?
915
+ @logger.log(Logger::ERROR, Helpers::Constants::ODP_LOGS[:ODP_INVALID_ACTION])
916
+ return
917
+ end
918
+
919
+ type = Helpers::Constants::ODP_MANAGER_CONFIG[:EVENT_TYPE] if type.nil? || type.empty?
920
+
921
+ @odp_manager.send_event(type: type, action: action, identifiers: identifiers, data: data)
922
+ end
923
+
924
+ def identify_user(user_id:)
925
+ unless is_valid
926
+ @logger.log(Logger::ERROR, InvalidProjectConfigError.new('identify_user').message)
927
+ return
928
+ end
929
+
930
+ @odp_manager.identify_user(user_id: user_id)
931
+ end
932
+
933
+ def fetch_qualified_segments(user_id:, options: [])
934
+ unless is_valid
935
+ @logger.log(Logger::ERROR, InvalidProjectConfigError.new('fetch_qualified_segments').message)
936
+ return
937
+ end
938
+
939
+ @odp_manager.fetch_qualified_segments(user_id: user_id, options: options)
940
+ end
941
+
872
942
  private
873
943
 
874
944
  def get_variation_with_config(experiment_key, user_id, attributes, config)
@@ -888,7 +958,7 @@ module Optimizely
888
958
 
889
959
  return nil unless user_inputs_valid?(attributes)
890
960
 
891
- user_context = create_user_context(user_id, attributes)
961
+ user_context = OptimizelyUserContext.new(self, user_id, attributes, identify: false)
892
962
  variation_id, = @decision_service.get_variation(config, experiment_id, user_context)
893
963
  variation = config.get_variation_from_id(experiment_key, variation_id) unless variation_id.nil?
894
964
  variation_key = variation['key'] if variation
@@ -955,7 +1025,7 @@ module Optimizely
955
1025
  return nil
956
1026
  end
957
1027
 
958
- user_context = create_user_context(user_id, attributes)
1028
+ user_context = OptimizelyUserContext.new(self, user_id, attributes, identify: false)
959
1029
  decision, = @decision_service.get_variation_for_feature(config, feature_flag, user_context)
960
1030
  variation = decision ? decision['variation'] : nil
961
1031
  feature_enabled = variation ? variation['featureEnabled'] : false
@@ -1126,5 +1196,67 @@ module Optimizely
1126
1196
  def project_config
1127
1197
  @config_manager.config
1128
1198
  end
1199
+
1200
+ def update_odp_config_on_datafile_update
1201
+ # if datafile isn't ready, expects to be called again by the internal notification_center
1202
+ return if @config_manager.respond_to?(:ready?) && !@config_manager.ready?
1203
+
1204
+ config = @config_manager&.config
1205
+ return unless config
1206
+
1207
+ @odp_manager.update_odp_config(config.public_key_for_odp, config.host_for_odp, config.all_segments)
1208
+ end
1209
+
1210
+ def setup_odp!(sdk_key)
1211
+ unless @sdk_settings.is_a? Optimizely::Helpers::OptimizelySdkSettings
1212
+ @logger.log(Logger::DEBUG, 'Provided sdk_settings is not an OptimizelySdkSettings instance.') unless @sdk_settings.nil?
1213
+ @sdk_settings = Optimizely::Helpers::OptimizelySdkSettings.new
1214
+ end
1215
+
1216
+ if !@sdk_settings.odp_segment_manager.nil? && !Helpers::Validator.segment_manager_valid?(@sdk_settings.odp_segment_manager)
1217
+ @logger.log(Logger::ERROR, 'Invalid ODP segment manager, reverting to default.')
1218
+ @sdk_settings.odp_segment_manager = nil
1219
+ end
1220
+
1221
+ if !@sdk_settings.odp_event_manager.nil? && !Helpers::Validator.event_manager_valid?(@sdk_settings.odp_event_manager)
1222
+ @logger.log(Logger::ERROR, 'Invalid ODP event manager, reverting to default.')
1223
+ @sdk_settings.odp_event_manager = nil
1224
+ end
1225
+
1226
+ if !@sdk_settings.odp_segments_cache.nil? && !Helpers::Validator.segments_cache_valid?(@sdk_settings.odp_segments_cache)
1227
+ @logger.log(Logger::ERROR, 'Invalid ODP segments cache, reverting to default.')
1228
+ @sdk_settings.odp_segments_cache = nil
1229
+ end
1230
+
1231
+ # no need to instantiate a cache if a custom cache or segment manager is provided.
1232
+ if !@sdk_settings.odp_disabled && @sdk_settings.odp_segment_manager.nil?
1233
+ @sdk_settings.odp_segments_cache ||= LRUCache.new(
1234
+ @sdk_settings.segments_cache_size,
1235
+ @sdk_settings.segments_cache_timeout_in_secs
1236
+ )
1237
+ end
1238
+
1239
+ @odp_manager = OdpManager.new(
1240
+ disable: @sdk_settings.odp_disabled,
1241
+ segment_manager: @sdk_settings.odp_segment_manager,
1242
+ event_manager: @sdk_settings.odp_event_manager,
1243
+ segments_cache: @sdk_settings.odp_segments_cache,
1244
+ fetch_segments_timeout: @sdk_settings.fetch_segments_timeout,
1245
+ odp_event_timeout: @sdk_settings.odp_event_timeout,
1246
+ odp_flush_interval: @sdk_settings.odp_flush_interval,
1247
+ logger: @logger
1248
+ )
1249
+
1250
+ return if @sdk_settings.odp_disabled
1251
+
1252
+ Optimizely::NotificationCenterRegistry
1253
+ .get_notification_center(sdk_key, @logger)
1254
+ &.add_notification_listener(
1255
+ NotificationCenter::NOTIFICATION_TYPES[:OPTIMIZELY_CONFIG_UPDATE],
1256
+ method(:update_odp_config_on_datafile_update)
1257
+ )
1258
+
1259
+ update_odp_config_on_datafile_update
1260
+ end
1129
1261
  end
1130
1262
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: optimizely-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.1
4
+ version: 5.0.0.pre.beta
5
5
  platform: ruby
6
6
  authors:
7
7
  - Optimizely
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-03-13 00:00:00.000000000 Z
11
+ date: 2023-04-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -169,10 +169,20 @@ files:
169
169
  - lib/optimizely/helpers/event_tag_utils.rb
170
170
  - lib/optimizely/helpers/group.rb
171
171
  - lib/optimizely/helpers/http_utils.rb
172
+ - lib/optimizely/helpers/sdk_settings.rb
172
173
  - lib/optimizely/helpers/validator.rb
173
174
  - lib/optimizely/helpers/variable_type.rb
174
175
  - lib/optimizely/logger.rb
175
176
  - lib/optimizely/notification_center.rb
177
+ - lib/optimizely/notification_center_registry.rb
178
+ - lib/optimizely/odp/lru_cache.rb
179
+ - lib/optimizely/odp/odp_config.rb
180
+ - lib/optimizely/odp/odp_event.rb
181
+ - lib/optimizely/odp/odp_event_api_manager.rb
182
+ - lib/optimizely/odp/odp_event_manager.rb
183
+ - lib/optimizely/odp/odp_manager.rb
184
+ - lib/optimizely/odp/odp_segment_api_manager.rb
185
+ - lib/optimizely/odp/odp_segment_manager.rb
176
186
  - lib/optimizely/optimizely_config.rb
177
187
  - lib/optimizely/optimizely_factory.rb
178
188
  - lib/optimizely/optimizely_user_context.rb
@@ -197,9 +207,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
197
207
  version: '2.7'
198
208
  required_rubygems_version: !ruby/object:Gem::Requirement
199
209
  requirements:
200
- - - ">="
210
+ - - ">"
201
211
  - !ruby/object:Gem::Version
202
- version: '0'
212
+ version: 1.3.1
203
213
  requirements: []
204
214
  rubygems_version: 3.3.7
205
215
  signing_key: