optimizely-sdk 4.0.1 → 5.0.0.pre.beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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: