optimizely-sdk 4.0.1 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/lib/optimizely/audience.rb +7 -7
  4. data/lib/optimizely/bucketer.rb +2 -2
  5. data/lib/optimizely/config/datafile_project_config.rb +58 -39
  6. data/lib/optimizely/config_manager/http_project_config_manager.rb +20 -10
  7. data/lib/optimizely/config_manager/project_config_manager.rb +2 -1
  8. data/lib/optimizely/config_manager/static_project_config_manager.rb +5 -3
  9. data/lib/optimizely/event/event_factory.rb +2 -2
  10. data/lib/optimizely/event_builder.rb +13 -13
  11. data/lib/optimizely/event_dispatcher.rb +2 -4
  12. data/lib/optimizely/exceptions.rb +69 -11
  13. data/lib/optimizely/helpers/constants.rb +45 -1
  14. data/lib/optimizely/helpers/http_utils.rb +3 -0
  15. data/lib/optimizely/helpers/sdk_settings.rb +61 -0
  16. data/lib/optimizely/helpers/validator.rb +54 -1
  17. data/lib/optimizely/notification_center_registry.rb +71 -0
  18. data/lib/optimizely/odp/lru_cache.rb +114 -0
  19. data/lib/optimizely/odp/odp_config.rb +102 -0
  20. data/lib/optimizely/odp/odp_event.rb +75 -0
  21. data/lib/optimizely/odp/odp_event_api_manager.rb +70 -0
  22. data/lib/optimizely/odp/odp_event_manager.rb +286 -0
  23. data/lib/optimizely/odp/odp_manager.rb +159 -0
  24. data/lib/optimizely/odp/odp_segment_api_manager.rb +122 -0
  25. data/lib/optimizely/odp/odp_segment_manager.rb +97 -0
  26. data/lib/optimizely/optimizely_config.rb +4 -2
  27. data/lib/optimizely/optimizely_factory.rb +17 -14
  28. data/lib/optimizely/optimizely_user_context.rb +40 -6
  29. data/lib/optimizely/user_condition_evaluator.rb +1 -1
  30. data/lib/optimizely/version.rb +2 -2
  31. data/lib/optimizely.rb +155 -23
  32. metadata +15 -5
@@ -0,0 +1,286 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright 2019, 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
+ require_relative 'odp_event_api_manager'
19
+ require_relative '../helpers/constants'
20
+ require_relative 'odp_event'
21
+
22
+ module Optimizely
23
+ class OdpEventManager
24
+ # Events passed to the OdpEventManager are immediately added to an EventQueue.
25
+ # The OdpEventManager maintains a single consumer thread that pulls events off of
26
+ # the BlockingQueue and buffers them for either a configured batch size or for a
27
+ # maximum duration before the resulting OdpEvent is sent to Odp.
28
+
29
+ attr_reader :batch_size, :api_manager, :logger
30
+ attr_accessor :odp_config
31
+
32
+ def initialize(
33
+ api_manager: nil,
34
+ logger: NoOpLogger.new,
35
+ proxy_config: nil,
36
+ request_timeout: nil,
37
+ flush_interval: nil
38
+ )
39
+ @odp_config = nil
40
+ @api_host = nil
41
+ @api_key = nil
42
+
43
+ @mutex = Mutex.new
44
+ @event_queue = SizedQueue.new(Optimizely::Helpers::Constants::ODP_EVENT_MANAGER[:DEFAULT_QUEUE_CAPACITY])
45
+ @queue_capacity = Helpers::Constants::ODP_EVENT_MANAGER[:DEFAULT_QUEUE_CAPACITY]
46
+ # received signal should be sent after adding item to event_queue
47
+ @received = ConditionVariable.new
48
+ @logger = logger
49
+ @api_manager = api_manager || OdpEventApiManager.new(logger: @logger, proxy_config: proxy_config, timeout: request_timeout)
50
+ @flush_interval = flush_interval || Helpers::Constants::ODP_EVENT_MANAGER[:DEFAULT_FLUSH_INTERVAL_SECONDS]
51
+ @batch_size = @flush_interval&.zero? ? 1 : Helpers::Constants::ODP_EVENT_MANAGER[:DEFAULT_BATCH_SIZE]
52
+ @flush_deadline = 0
53
+ @retry_count = Helpers::Constants::ODP_EVENT_MANAGER[:DEFAULT_RETRY_COUNT]
54
+ # current_batch should only be accessed by processing thread
55
+ @current_batch = []
56
+ @thread = nil
57
+ @thread_exception = false
58
+ end
59
+
60
+ def start!(odp_config)
61
+ if running?
62
+ @logger.log(Logger::WARN, 'Service already started.')
63
+ return
64
+ end
65
+
66
+ @odp_config = odp_config
67
+ @api_host = odp_config.api_host
68
+ @api_key = odp_config.api_key
69
+
70
+ @thread = Thread.new { run }
71
+ @logger.log(Logger::INFO, 'Starting scheduler.')
72
+ end
73
+
74
+ def flush
75
+ begin
76
+ @event_queue.push(:FLUSH_SIGNAL, true)
77
+ rescue ThreadError
78
+ @logger.log(Logger::ERROR, 'Error flushing ODP event queue.')
79
+ return
80
+ end
81
+
82
+ @mutex.synchronize do
83
+ @received.signal
84
+ end
85
+ end
86
+
87
+ def update_config
88
+ begin
89
+ # Adds update config signal to event_queue.
90
+ @event_queue.push(:UPDATE_CONFIG, true)
91
+ rescue ThreadError
92
+ @logger.log(Logger::ERROR, 'Error updating ODP config for the event queue')
93
+ end
94
+
95
+ @mutex.synchronize do
96
+ @received.signal
97
+ end
98
+ end
99
+
100
+ def dispatch(event)
101
+ if @thread_exception
102
+ @logger.log(Logger::ERROR, format(Helpers::Constants::ODP_LOGS[:ODP_EVENT_FAILED], 'Queue is down'))
103
+ return
104
+ end
105
+
106
+ # if the processor has been explicitly stopped. Don't accept tasks
107
+ unless running?
108
+ @logger.log(Logger::WARN, 'ODP event queue is shutdown, not accepting events.')
109
+ return
110
+ end
111
+
112
+ begin
113
+ @logger.log(Logger::DEBUG, 'ODP event queue: adding event.')
114
+ @event_queue.push(event, true)
115
+ rescue => e
116
+ @logger.log(Logger::WARN, format(Helpers::Constants::ODP_LOGS[:ODP_EVENT_FAILED], e.message))
117
+ return
118
+ end
119
+
120
+ @mutex.synchronize do
121
+ @received.signal
122
+ end
123
+ end
124
+
125
+ def send_event(type:, action:, identifiers:, data:)
126
+ case @odp_config&.odp_state
127
+ when nil
128
+ @logger.log(Logger::DEBUG, 'ODP event queue: cannot send before config has been set.')
129
+ return
130
+ when OdpConfig::ODP_CONFIG_STATE[:UNDETERMINED]
131
+ @logger.log(Logger::DEBUG, 'ODP event queue: cannot send before the datafile has loaded.')
132
+ return
133
+ when OdpConfig::ODP_CONFIG_STATE[:NOT_INTEGRATED]
134
+ @logger.log(Logger::DEBUG, Helpers::Constants::ODP_LOGS[:ODP_NOT_INTEGRATED])
135
+ return
136
+ end
137
+
138
+ event = Optimizely::OdpEvent.new(type: type, action: action, identifiers: identifiers, data: data)
139
+ dispatch(event)
140
+ end
141
+
142
+ def stop!
143
+ return unless running?
144
+
145
+ begin
146
+ @event_queue.push(:SHUTDOWN_SIGNAL, true)
147
+ rescue ThreadError
148
+ @logger.log(Logger::ERROR, 'Error stopping ODP event queue.')
149
+ return
150
+ end
151
+
152
+ @event_queue.close
153
+
154
+ @mutex.synchronize do
155
+ @received.signal
156
+ end
157
+
158
+ @logger.log(Logger::INFO, 'Stopping ODP event queue.')
159
+
160
+ @thread.join
161
+
162
+ @logger.log(Logger::ERROR, format(Helpers::Constants::ODP_LOGS[:ODP_EVENT_FAILED], @current_batch.to_json)) unless @current_batch.empty?
163
+ end
164
+
165
+ def running?
166
+ !!@thread && !!@thread.status && !@event_queue.closed?
167
+ end
168
+
169
+ private
170
+
171
+ def run
172
+ loop do
173
+ @mutex.synchronize do
174
+ @received.wait(@mutex, queue_timeout) if @event_queue.empty?
175
+ end
176
+
177
+ begin
178
+ item = @event_queue.pop(true)
179
+ rescue ThreadError => e
180
+ raise unless e.message == 'queue empty'
181
+
182
+ item = nil
183
+ end
184
+
185
+ case item
186
+ when :SHUTDOWN_SIGNAL
187
+ @logger.log(Logger::DEBUG, 'ODP event queue: received shutdown signal.')
188
+ break
189
+
190
+ when :FLUSH_SIGNAL
191
+ @logger.log(Logger::DEBUG, 'ODP event queue: received flush signal.')
192
+ flush_batch!
193
+
194
+ when :UPDATE_CONFIG
195
+ @logger.log(Logger::DEBUG, 'ODP event queue: received update config signal.')
196
+ process_config_update
197
+
198
+ when Optimizely::OdpEvent
199
+ add_to_batch(item)
200
+
201
+ when nil && !@current_batch.empty?
202
+ @logger.log(Logger::DEBUG, 'ODP event queue: flushing on interval.')
203
+ flush_batch!
204
+ end
205
+ end
206
+ rescue SignalException
207
+ @thread_exception = true
208
+ @logger.log(Logger::ERROR, 'Interrupted while processing ODP events.')
209
+ rescue => e
210
+ @thread_exception = true
211
+ @logger.log(Logger::ERROR, "Uncaught exception processing ODP events. Error: #{e.message}")
212
+ ensure
213
+ @logger.log(Logger::INFO, 'Exiting ODP processing loop. Attempting to flush pending events.')
214
+ flush_batch!
215
+ end
216
+
217
+ def flush_batch!
218
+ if @current_batch.empty?
219
+ @logger.log(Logger::DEBUG, 'ODP event queue: nothing to flush.')
220
+ return
221
+ end
222
+
223
+ if @api_key.nil? || @api_host.nil?
224
+ @logger.log(Logger::DEBUG, Helpers::Constants::ODP_LOGS[:ODP_NOT_INTEGRATED])
225
+ @current_batch.clear
226
+ return
227
+ end
228
+
229
+ @logger.log(Logger::DEBUG, "ODP event queue: flushing batch size #{@current_batch.length}.")
230
+ should_retry = false
231
+
232
+ i = 0
233
+ while i < @retry_count
234
+ begin
235
+ should_retry = @api_manager.send_odp_events(@api_key, @api_host, @current_batch)
236
+ rescue StandardError => e
237
+ should_retry = false
238
+ @logger.log(Logger::ERROR, format(Helpers::Constants::ODP_LOGS[:ODP_EVENT_FAILED], "Error: #{e.message} #{@current_batch.to_json}"))
239
+ end
240
+ break unless should_retry
241
+
242
+ @logger.log(Logger::DEBUG, 'Error dispatching ODP events, scheduled to retry.') if i < @retry_count
243
+ i += 1
244
+ end
245
+
246
+ @logger.log(Logger::ERROR, format(Helpers::Constants::ODP_LOGS[:ODP_EVENT_FAILED], "Failed after #{i} retries: #{@current_batch.to_json}")) if should_retry
247
+
248
+ @current_batch.clear
249
+ end
250
+
251
+ def add_to_batch(event)
252
+ set_flush_deadline if @current_batch.empty?
253
+
254
+ @current_batch << event
255
+ return unless @current_batch.length >= @batch_size
256
+
257
+ @logger.log(Logger::DEBUG, 'ODP event queue: flushing on batch size.')
258
+ flush_batch!
259
+ end
260
+
261
+ def set_flush_deadline
262
+ # Sets time that next flush will occur.
263
+ @flush_deadline = Time.new + @flush_interval
264
+ end
265
+
266
+ def time_till_flush
267
+ # Returns seconds until next flush; no less than 0.
268
+ [0, @flush_deadline - Time.new].max
269
+ end
270
+
271
+ def queue_timeout
272
+ # Returns seconds until next flush or None if current batch is empty.
273
+ return nil if @current_batch.empty?
274
+
275
+ time_till_flush
276
+ end
277
+
278
+ def process_config_update
279
+ # Updates the configuration used to send events.
280
+ flush_batch! unless @current_batch.empty?
281
+
282
+ @api_key = @odp_config&.api_key
283
+ @api_host = @odp_config&.api_host
284
+ end
285
+ end
286
+ end
@@ -0,0 +1,159 @@
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 '../helpers/constants'
21
+ require_relative '../helpers/validator'
22
+ require_relative '../exceptions'
23
+ require_relative 'odp_config'
24
+ require_relative 'lru_cache'
25
+ require_relative 'odp_segment_manager'
26
+ require_relative 'odp_event_manager'
27
+
28
+ module Optimizely
29
+ class OdpManager
30
+ ODP_LOGS = Helpers::Constants::ODP_LOGS
31
+ ODP_MANAGER_CONFIG = Helpers::Constants::ODP_MANAGER_CONFIG
32
+ ODP_CONFIG_STATE = Helpers::Constants::ODP_CONFIG_STATE
33
+
34
+ # update_odp_config must be called to complete initialization
35
+ def initialize(
36
+ disable:,
37
+ segments_cache: nil,
38
+ segment_manager: nil,
39
+ event_manager: nil,
40
+ fetch_segments_timeout: nil,
41
+ odp_event_timeout: nil,
42
+ odp_flush_interval: nil,
43
+ logger: nil
44
+ )
45
+ @enabled = !disable
46
+ @segment_manager = segment_manager
47
+ @event_manager = event_manager
48
+ @logger = logger || NoOpLogger.new
49
+ @odp_config = OdpConfig.new
50
+
51
+ unless @enabled
52
+ @logger.log(Logger::INFO, ODP_LOGS[:ODP_NOT_ENABLED])
53
+ return
54
+ end
55
+
56
+ unless @segment_manager
57
+ segments_cache ||= LRUCache.new(
58
+ Helpers::Constants::ODP_SEGMENTS_CACHE_CONFIG[:DEFAULT_CAPACITY],
59
+ Helpers::Constants::ODP_SEGMENTS_CACHE_CONFIG[:DEFAULT_TIMEOUT_SECONDS]
60
+ )
61
+ @segment_manager = Optimizely::OdpSegmentManager.new(segments_cache, nil, @logger, timeout: fetch_segments_timeout)
62
+ end
63
+
64
+ @event_manager ||= Optimizely::OdpEventManager.new(logger: @logger, request_timeout: odp_event_timeout, flush_interval: odp_flush_interval)
65
+
66
+ @segment_manager.odp_config = @odp_config
67
+ end
68
+
69
+ def fetch_qualified_segments(user_id:, options:)
70
+ # Returns qualified segments for the user from the cache or the ODP server if not in the cache.
71
+ #
72
+ # @param user_id - The user id.
73
+ # @param options - An array of OptimizelySegmentOptions used to ignore and/or reset the cache.
74
+ #
75
+ # @return - Array of qualified segments or nil.
76
+ options ||= []
77
+ unless @enabled
78
+ @logger.log(Logger::ERROR, ODP_LOGS[:ODP_NOT_ENABLED])
79
+ return nil
80
+ end
81
+
82
+ if @odp_config.odp_state == ODP_CONFIG_STATE[:UNDETERMINED]
83
+ @logger.log(Logger::ERROR, 'Cannot fetch segments before the datafile has loaded.')
84
+ return nil
85
+ end
86
+
87
+ @segment_manager.fetch_qualified_segments(ODP_MANAGER_CONFIG[:KEY_FOR_USER_ID], user_id, options)
88
+ end
89
+
90
+ def identify_user(user_id:)
91
+ unless @enabled
92
+ @logger.log(Logger::DEBUG, 'ODP identify event is not dispatched (ODP disabled).')
93
+ return
94
+ end
95
+
96
+ case @odp_config.odp_state
97
+ when ODP_CONFIG_STATE[:UNDETERMINED]
98
+ @logger.log(Logger::DEBUG, 'ODP identify event is not dispatched (datafile not ready).')
99
+ return
100
+ when ODP_CONFIG_STATE[:NOT_INTEGRATED]
101
+ @logger.log(Logger::DEBUG, 'ODP identify event is not dispatched (ODP not integrated).')
102
+ return
103
+ end
104
+
105
+ @event_manager.send_event(
106
+ type: ODP_MANAGER_CONFIG[:EVENT_TYPE],
107
+ action: 'identified',
108
+ identifiers: {ODP_MANAGER_CONFIG[:KEY_FOR_USER_ID] => user_id},
109
+ data: {}
110
+ )
111
+ end
112
+
113
+ def send_event(type:, action:, identifiers:, data:)
114
+ # Send an event to the ODP server.
115
+ #
116
+ # @param type - the event type.
117
+ # @param action - the event action name.
118
+ # @param identifiers - a hash for identifiers.
119
+ # @param data - a hash for associated data. The default event data will be added to this data before sending to the ODP server.
120
+ unless @enabled
121
+ @logger.log(Logger::ERROR, ODP_LOGS[:ODP_NOT_ENABLED])
122
+ return
123
+ end
124
+
125
+ unless Helpers::Validator.odp_data_types_valid?(data)
126
+ @logger.log(Logger::ERROR, ODP_LOGS[:ODP_INVALID_DATA])
127
+ return
128
+ end
129
+
130
+ @event_manager.send_event(type: type, action: action, identifiers: identifiers, data: data)
131
+ end
132
+
133
+ def update_odp_config(api_key, api_host, segments_to_check)
134
+ # Update the odp config, reset the cache and send signal to the event processor to update its config.
135
+ # Start the event manager if odp is integrated.
136
+ return unless @enabled
137
+
138
+ config_changed = @odp_config.update(api_key, api_host, segments_to_check)
139
+ unless config_changed
140
+ @logger.log(Logger::DEBUG, 'Odp config was not changed.')
141
+ return
142
+ end
143
+
144
+ @segment_manager.reset
145
+
146
+ if @event_manager.running?
147
+ @event_manager.update_config
148
+ elsif @odp_config.odp_state == ODP_CONFIG_STATE[:INTEGRATED]
149
+ @event_manager.start!(@odp_config)
150
+ end
151
+ end
152
+
153
+ def stop!
154
+ return unless @enabled
155
+
156
+ @event_manager.stop!
157
+ end
158
+ end
159
+ end
@@ -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
@@ -19,8 +19,9 @@ module Optimizely
19
19
  require 'json'
20
20
  class OptimizelyConfig
21
21
  include Optimizely::ConditionTreeEvaluator
22
- def initialize(project_config)
22
+ def initialize(project_config, logger = nil)
23
23
  @project_config = project_config
24
+ @logger = logger || NoOpLogger.new
24
25
  @rollouts = @project_config.rollouts
25
26
  @audiences = []
26
27
  audience_id_lookup_dict = {}
@@ -91,6 +92,7 @@ module Optimizely
91
92
 
92
93
  def experiments_map
93
94
  experiments_id_map.values.reduce({}) do |experiments_key_map, experiment|
95
+ @logger.log(Logger::WARN, "Duplicate experiment keys found in datafile: #{experiment['key']}") if experiments_key_map.key? experiment['key']
94
96
  experiments_key_map.update(experiment['key'] => experiment)
95
97
  end
96
98
  end
@@ -201,7 +203,7 @@ module Optimizely
201
203
  def stringify_conditions(conditions, audiences_map)
202
204
  operand = 'OR'
203
205
  conditions_str = ''
204
- length = conditions.length()
206
+ length = conditions.length
205
207
  return '' if length.zero?
206
208
  return "\"#{lookup_name_from_id(conditions[0], audiences_map)}\"" if length == 1 && !OPERATORS.include?(conditions[0])
207
209