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
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  #
4
- # Copyright 2016-2020, Optimizely and contributors
4
+ # Copyright 2016-2020, 2022, 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.
@@ -382,6 +382,15 @@ module Optimizely
382
382
  'EVALUATING_AUDIENCES_COMBINED' => "Evaluating audiences for rule '%s': %s."
383
383
  }.merge(AUDIENCE_EVALUATION_LOGS).freeze
384
384
 
385
+ ODP_LOGS = {
386
+ FETCH_SEGMENTS_FAILED: 'Audience segments fetch failed (%s).',
387
+ ODP_EVENT_FAILED: 'ODP event send failed (%s).',
388
+ ODP_NOT_ENABLED: 'ODP is not enabled.',
389
+ ODP_NOT_INTEGRATED: 'ODP is not integrated.',
390
+ ODP_INVALID_DATA: 'ODP data is not valid.',
391
+ ODP_INVALID_ACTION: 'ODP action is not valid (cannot be empty).'
392
+ }.freeze
393
+
385
394
  DECISION_NOTIFICATION_TYPES = {
386
395
  'AB_TEST' => 'ab-test',
387
396
  'FEATURE' => 'feature',
@@ -406,6 +415,41 @@ module Optimizely
406
415
  'REQUEST_TIMEOUT' => 10
407
416
  }.freeze
408
417
 
418
+ EVENT_DISPATCH_CONFIG = {
419
+ REQUEST_TIMEOUT: 10
420
+ }.freeze
421
+
422
+ ODP_GRAPHQL_API_CONFIG = {
423
+ REQUEST_TIMEOUT: 10
424
+ }.freeze
425
+
426
+ ODP_REST_API_CONFIG = {
427
+ REQUEST_TIMEOUT: 10
428
+ }.freeze
429
+
430
+ ODP_SEGMENTS_CACHE_CONFIG = {
431
+ DEFAULT_CAPACITY: 10_000,
432
+ DEFAULT_TIMEOUT_SECONDS: 600
433
+ }.freeze
434
+
435
+ ODP_MANAGER_CONFIG = {
436
+ KEY_FOR_USER_ID: 'fs_user_id',
437
+ EVENT_TYPE: 'fullstack'
438
+ }.freeze
439
+
440
+ ODP_CONFIG_STATE = {
441
+ UNDETERMINED: 'UNDETERMINED',
442
+ INTEGRATED: 'INTEGRATED',
443
+ NOT_INTEGRATED: 'NOT_INTEGRATED'
444
+ }.freeze
445
+
446
+ ODP_EVENT_MANAGER = {
447
+ DEFAULT_QUEUE_CAPACITY: 10_000,
448
+ DEFAULT_BATCH_SIZE: 10,
449
+ DEFAULT_FLUSH_INTERVAL_SECONDS: 1,
450
+ DEFAULT_RETRY_COUNT: 3
451
+ }.freeze
452
+
409
453
  HTTP_HEADERS = {
410
454
  'IF_MODIFIED_SINCE' => 'If-Modified-Since',
411
455
  'LAST_MODIFIED' => 'Last-Modified'
@@ -17,6 +17,7 @@
17
17
  #
18
18
 
19
19
  require 'net/http'
20
+ require_relative '../exceptions'
20
21
 
21
22
  module Optimizely
22
23
  module Helpers
@@ -28,6 +29,8 @@ module Optimizely
28
29
  #
29
30
  uri = URI.parse(url)
30
31
 
32
+ raise HTTPUriError unless uri.respond_to?(:request_uri)
33
+
31
34
  case http_method
32
35
  when :get
33
36
  request = Net::HTTP::Get.new(uri.request_uri)
@@ -0,0 +1,61 @@
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_relative 'constants'
20
+
21
+ module Optimizely
22
+ module Helpers
23
+ class OptimizelySdkSettings
24
+ attr_accessor :odp_disabled, :segments_cache_size, :segments_cache_timeout_in_secs, :odp_segments_cache, :odp_segment_manager,
25
+ :odp_event_manager, :fetch_segments_timeout, :odp_event_timeout, :odp_flush_interval
26
+
27
+ # Contains configuration used for Optimizely Project initialization.
28
+ #
29
+ # @param disable_odp - Set this flag to true (default = false) to disable ODP features.
30
+ # @param segments_cache_size - The maximum size of audience segments cache (optional. default = 10,000). Set to zero to disable caching.
31
+ # @param segments_cache_timeout_in_secs - The timeout in seconds of audience segments cache (optional. default = 600). Set to zero to disable timeout.
32
+ # @param odp_segments_cache - A custom odp segments cache. Required methods include: `save(key, value)`, `lookup(key) -> value`, and `reset()`
33
+ # @param odp_segment_manager - A custom odp segment manager. Required method is: `fetch_qualified_segments(user_key, user_value, options)`.
34
+ # @param odp_event_manager - A custom odp event manager. Required method is: `send_event(type:, action:, identifiers:, data:)`
35
+ # @param odp_segment_request_timeout - Time to wait in seconds for fetch_qualified_segments (optional. default = 10).
36
+ # @param odp_event_request_timeout - Time to wait in seconds for send_odp_events (optional. default = 10).
37
+ # @param odp_event_flush_interval - Time to wait in seconds for odp events to accumulate before sending (optional. default = 1).
38
+ def initialize(
39
+ disable_odp: false,
40
+ segments_cache_size: Constants::ODP_SEGMENTS_CACHE_CONFIG[:DEFAULT_CAPACITY],
41
+ segments_cache_timeout_in_secs: Constants::ODP_SEGMENTS_CACHE_CONFIG[:DEFAULT_TIMEOUT_SECONDS],
42
+ odp_segments_cache: nil,
43
+ odp_segment_manager: nil,
44
+ odp_event_manager: nil,
45
+ odp_segment_request_timeout: nil,
46
+ odp_event_request_timeout: nil,
47
+ odp_event_flush_interval: nil
48
+ )
49
+ @odp_disabled = disable_odp
50
+ @segments_cache_size = segments_cache_size
51
+ @segments_cache_timeout_in_secs = segments_cache_timeout_in_secs
52
+ @odp_segments_cache = odp_segments_cache
53
+ @odp_segment_manager = odp_segment_manager
54
+ @odp_event_manager = odp_event_manager
55
+ @fetch_segments_timeout = odp_segment_request_timeout
56
+ @odp_event_timeout = odp_event_request_timeout
57
+ @odp_flush_interval = odp_event_flush_interval
58
+ end
59
+ end
60
+ end
61
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  #
4
- # Copyright 2016-2019, 2022, Optimizely and contributors
4
+ # Copyright 2016-2019, 2022-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.
@@ -178,6 +178,59 @@ module Optimizely
178
178
 
179
179
  value.is_a?(Numeric) && value.to_f.finite? && value.abs <= Constants::FINITE_NUMBER_LIMIT
180
180
  end
181
+
182
+ def odp_data_types_valid?(data)
183
+ valid_types = [String, Float, Integer, TrueClass, FalseClass, NilClass]
184
+ data&.values&.all? { |e| valid_types.member? e.class }
185
+ end
186
+
187
+ def segments_cache_valid?(segments_cache)
188
+ # Determines if a given segments_cache is valid.
189
+ #
190
+ # segments_cache - custom cache to be validated.
191
+ #
192
+ # Returns boolean depending on whether cache has required methods.
193
+
194
+ segments_cache.respond_to?(:reset) &&
195
+ segments_cache.method(:reset)&.parameters&.empty? &&
196
+ segments_cache.respond_to?(:lookup) &&
197
+ segments_cache.method(:lookup)&.parameters&.length&.positive? &&
198
+ segments_cache.respond_to?(:save) &&
199
+ segments_cache.method(:save)&.parameters&.length&.positive?
200
+ end
201
+
202
+ def segment_manager_valid?(segment_manager)
203
+ # Determines if a given segment_manager is valid.
204
+ #
205
+ # segment_manager - custom manager to be validated.
206
+ #
207
+ # Returns boolean depending on whether manager has required methods.
208
+
209
+ segment_manager.respond_to?(:odp_config) &&
210
+ segment_manager.respond_to?(:reset) &&
211
+ segment_manager.method(:reset)&.parameters&.empty? &&
212
+ segment_manager.respond_to?(:fetch_qualified_segments) &&
213
+ (segment_manager.method(:fetch_qualified_segments)&.parameters&.length || 0) >= 3
214
+ end
215
+
216
+ def event_manager_valid?(event_manager)
217
+ # Determines if a given event_manager is valid.
218
+ #
219
+ # event_manager - custom manager to be validated.
220
+ #
221
+ # Returns boolean depending on whether manager has required method and parameters.
222
+ return false unless
223
+ event_manager.respond_to?(:send_event) &&
224
+ event_manager.respond_to?(:start!) &&
225
+ (event_manager.method(:start!)&.parameters&.length || 0) >= 1 &&
226
+ event_manager.respond_to?(:update_config) &&
227
+ event_manager.respond_to?(:stop!)
228
+
229
+ required_parameters = Set[%i[keyreq type], %i[keyreq action], %i[keyreq identifiers], %i[keyreq data]]
230
+ existing_parameters = event_manager.method(:send_event).parameters.to_set
231
+
232
+ existing_parameters & required_parameters == required_parameters
233
+ end
181
234
  end
182
235
  end
183
236
  end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright 2023, 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 'notification_center'
19
+ require_relative 'exceptions'
20
+
21
+ module Optimizely
22
+ class NotificationCenterRegistry
23
+ private_class_method :new
24
+ # Class managing internal notification centers.
25
+ # @api no-doc
26
+ @notification_centers = {}
27
+ @mutex = Mutex.new
28
+
29
+ # Returns an internal notification center for the given sdk_key, creating one
30
+ # if none exists yet.
31
+ #
32
+ # Args:
33
+ # sdk_key: A string sdk key to uniquely identify the notification center.
34
+ # logger: Optional logger.
35
+
36
+ # Returns:
37
+ # nil or NotificationCenter
38
+ def self.get_notification_center(sdk_key, logger)
39
+ unless sdk_key
40
+ logger&.log(Logger::ERROR, "#{MissingSdkKeyError.new.message} ODP may not work properly without it.")
41
+ return nil
42
+ end
43
+
44
+ notification_center = nil
45
+
46
+ @mutex.synchronize do
47
+ if @notification_centers.key?(sdk_key)
48
+ notification_center = @notification_centers[sdk_key]
49
+ else
50
+ notification_center = NotificationCenter.new(logger, nil)
51
+ @notification_centers[sdk_key] = notification_center
52
+ end
53
+ end
54
+
55
+ notification_center
56
+ end
57
+
58
+ # Remove a previously added notification center and clear all its listeners.
59
+
60
+ # Args:
61
+ # sdk_key: The sdk_key of the notification center to remove.
62
+ def self.remove_notification_center(sdk_key)
63
+ @mutex.synchronize do
64
+ @notification_centers
65
+ .delete(sdk_key)
66
+ &.clear_all_notification_listeners
67
+ end
68
+ nil
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,114 @@
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
+ module Optimizely
20
+ class LRUCache
21
+ # Least Recently Used cache that invalidates entries older than the timeout.
22
+
23
+ attr_reader :capacity, :timeout
24
+
25
+ def initialize(capacity, timeout_in_secs)
26
+ # @param capacity - The max size of the cache. If set <= 0, caching is disabled.
27
+ # @param timeout_in_secs - Seconds until a cache item is considered stale.
28
+ # If set <= 0, items never expire.
29
+ @cache_mutex = Mutex.new
30
+ @map = {}
31
+ @capacity = capacity
32
+ @timeout = timeout_in_secs
33
+ end
34
+
35
+ # Retrieve the non stale value from the cache corresponding to the provided key
36
+ # or nil if key is not found
37
+ # Moves the key/value pair to the end of the cache
38
+ #
39
+ # @param key - The key to retrieve
40
+
41
+ def lookup(key)
42
+ return nil if @capacity <= 0
43
+
44
+ @cache_mutex.synchronize do
45
+ return nil unless @map.include?(key)
46
+
47
+ element = @map.delete(key)
48
+ return nil if element.stale?(@timeout)
49
+
50
+ @map[key] = element
51
+
52
+ element.value
53
+ end
54
+ end
55
+
56
+ # Save a key/value pair into the cache
57
+ # Moves the key/value pair to the end of the cache
58
+ #
59
+ # @param key - A user key
60
+ # @param value - A user value
61
+
62
+ def save(key, value)
63
+ return if @capacity <= 0
64
+
65
+ @cache_mutex.synchronize do
66
+ @map.delete(key) if @map.key?(key)
67
+
68
+ @map[key] = CacheElement.new(value)
69
+
70
+ @map.delete(@map.first[0]) if @map.size > @capacity
71
+ nil
72
+ end
73
+ end
74
+
75
+ # Clears the cache
76
+
77
+ def reset
78
+ return if @capacity <= 0
79
+
80
+ @cache_mutex.synchronize { @map.clear }
81
+ nil
82
+ end
83
+
84
+ # Retrieve a value from the cache for a given key or nil if key is not found
85
+ # Doesn't update the cache
86
+ #
87
+ # @param key - The key to retrieve
88
+
89
+ def peek(key)
90
+ return nil if @capacity <= 0
91
+
92
+ @cache_mutex.synchronize { @map[key]&.value }
93
+ end
94
+ end
95
+
96
+ class CacheElement
97
+ # Individual element for the LRUCache.
98
+ attr_reader :value, :timestamp
99
+
100
+ def initialize(value)
101
+ @value = value
102
+ @timestamp = Time.new
103
+ end
104
+
105
+ def stale?(timeout)
106
+ # Returns true if the provided timeout has passed since the element's timestamp.
107
+ #
108
+ # @param timeout - The duration to check against
109
+ return false if timeout <= 0
110
+
111
+ Time.new - @timestamp >= timeout
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,102 @@
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
+
22
+ module Optimizely
23
+ class OdpConfig
24
+ ODP_CONFIG_STATE = Helpers::Constants::ODP_CONFIG_STATE
25
+ # Contains configuration used for ODP integration.
26
+ #
27
+ # @param api_host - The host URL for the ODP audience segments API (optional).
28
+ # @param api_key - The public API key for the ODP account from which the audience segments will be fetched (optional).
29
+ # @param segments_to_check - An array of all ODP segments used in the current datafile (associated with api_host/api_key).
30
+ def initialize(api_key = nil, api_host = nil, segments_to_check = [])
31
+ @api_key = api_key
32
+ @api_host = api_host
33
+ @segments_to_check = segments_to_check
34
+ @mutex = Mutex.new
35
+ @odp_state = @api_host.nil? || @api_key.nil? ? ODP_CONFIG_STATE[:UNDETERMINED] : ODP_CONFIG_STATE[:INTEGRATED]
36
+ end
37
+
38
+ # Replaces the existing configuration
39
+ #
40
+ # @param api_host - The host URL for the ODP audience segments API (optional).
41
+ # @param api_key - The public API key for the ODP account from which the audience segments will be fetched (optional).
42
+ # @param segments_to_check - An array of all ODP segments used in the current datafile (associated with api_host/api_key).
43
+ #
44
+ # @return - True if the provided values were different than the existing values.
45
+
46
+ def update(api_key = nil, api_host = nil, segments_to_check = [])
47
+ updated = false
48
+ @mutex.synchronize do
49
+ @odp_state = api_host.nil? || api_key.nil? ? ODP_CONFIG_STATE[:NOT_INTEGRATED] : ODP_CONFIG_STATE[:INTEGRATED]
50
+
51
+ if @api_key != api_key || @api_host != api_host || @segments_to_check != segments_to_check
52
+ @api_key = api_key
53
+ @api_host = api_host
54
+ @segments_to_check = segments_to_check
55
+ updated = true
56
+ end
57
+ end
58
+
59
+ updated
60
+ end
61
+
62
+ # Returns the api host for odp connections
63
+ #
64
+ # @return - The api host.
65
+
66
+ def api_host
67
+ @mutex.synchronize { @api_host.clone }
68
+ end
69
+
70
+ # Returns the api key for odp connections
71
+ #
72
+ # @return - The api key.
73
+
74
+ def api_key
75
+ @mutex.synchronize { @api_key.clone }
76
+ end
77
+
78
+ # Returns An array of qualified segments for this user
79
+ #
80
+ # @return - An array of segments names.
81
+
82
+ def segments_to_check
83
+ @mutex.synchronize { @segments_to_check.clone }
84
+ end
85
+
86
+ # Replace qualified segments with provided segments
87
+ #
88
+ # @param segments - An array of segment names
89
+
90
+ def segments_to_check=(segments_to_check)
91
+ @mutex.synchronize { @segments_to_check = segments_to_check.clone }
92
+ end
93
+
94
+ # Returns the state of odp integration (UNDETERMINED, INTEGRATED, NOT_INTEGRATED)
95
+ #
96
+ # @return - string
97
+
98
+ def odp_state
99
+ @mutex.synchronize { @odp_state }
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,75 @@
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 '../helpers/constants'
21
+
22
+ module Optimizely
23
+ class OdpEvent
24
+ # Representation of an odp event which can be sent to the Optimizely odp platform.
25
+
26
+ KEY_FOR_USER_ID = Helpers::Constants::ODP_MANAGER_CONFIG[:KEY_FOR_USER_ID]
27
+
28
+ def initialize(type:, action:, identifiers:, data:)
29
+ @type = type
30
+ @action = action
31
+ @identifiers = convert_identifiers(identifiers)
32
+ @data = add_common_event_data(data)
33
+ end
34
+
35
+ def add_common_event_data(custom_data)
36
+ data = {
37
+ idempotence_id: SecureRandom.uuid,
38
+ data_source_type: 'sdk',
39
+ data_source: 'ruby-sdk',
40
+ data_source_version: VERSION
41
+ }
42
+ data.update(custom_data)
43
+ data
44
+ end
45
+
46
+ def convert_identifiers(identifiers)
47
+ # Convert incorrect case/separator of identifier key `fs_user_id`
48
+ # (ie. `fs-user-id`, `FS_USER_ID`).
49
+
50
+ identifiers.clone.each_key do |key|
51
+ break if key == KEY_FOR_USER_ID
52
+
53
+ if ['fs-user-id', KEY_FOR_USER_ID].include?(key.downcase)
54
+ identifiers[KEY_FOR_USER_ID] = identifiers.delete(key)
55
+ break
56
+ end
57
+ end
58
+
59
+ identifiers
60
+ end
61
+
62
+ def to_json(*_args)
63
+ {
64
+ type: @type,
65
+ action: @action,
66
+ identifiers: @identifiers,
67
+ data: @data
68
+ }.to_json
69
+ end
70
+
71
+ def ==(other)
72
+ to_json == other.to_json
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,70 @@
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
+
21
+ module Optimizely
22
+ class OdpEventApiManager
23
+ # Interface that handles sending ODP events.
24
+
25
+ def initialize(logger: nil, proxy_config: nil, timeout: nil)
26
+ @logger = logger || NoOpLogger.new
27
+ @proxy_config = proxy_config
28
+ @timeout = timeout || Optimizely::Helpers::Constants::ODP_REST_API_CONFIG[:REQUEST_TIMEOUT]
29
+ end
30
+
31
+ # Send events to the ODP Events API.
32
+ #
33
+ # @param api_key - public api key
34
+ # @param api_host - domain url of the host
35
+ # @param events - array of events to send
36
+
37
+ def send_odp_events(api_key, api_host, events)
38
+ should_retry = false
39
+ url = "#{api_host}/v3/events"
40
+
41
+ headers = {'Content-Type' => 'application/json', 'x-api-key' => api_key.to_s}
42
+
43
+ begin
44
+ response = Helpers::HttpUtils.make_request(
45
+ url, :post, events.to_json, headers, @timeout, @proxy_config
46
+ )
47
+ rescue SocketError, Timeout::Error, Errno::ECONNRESET, Errno::EHOSTUNREACH, Errno::EFAULT, Errno::ENETUNREACH, Errno::ENETDOWN, Errno::ECONNREFUSED
48
+ log_failure('network error')
49
+ should_retry = true
50
+ return should_retry
51
+ rescue StandardError => e
52
+ log_failure(e)
53
+ return should_retry
54
+ end
55
+
56
+ status = response.code.to_i
57
+ if status >= 400
58
+ log_failure(!response.body.empty? ? response.body : "#{status}: #{response.message}")
59
+ should_retry = status >= 500
60
+ end
61
+ should_retry
62
+ end
63
+
64
+ private
65
+
66
+ def log_failure(message, level = Logger::ERROR)
67
+ @logger.log(level, format(Optimizely::Helpers::Constants::ODP_LOGS[:ODP_EVENT_FAILED], message))
68
+ end
69
+ end
70
+ end