optimizely-sdk 5.0.1 → 5.2.0

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.
@@ -190,4 +190,28 @@ module Optimizely
190
190
  super
191
191
  end
192
192
  end
193
+
194
+ class CmabError < Error
195
+ # Base exception for CMAB errors
196
+
197
+ def initialize(msg = 'CMAB error occurred.')
198
+ super
199
+ end
200
+ end
201
+
202
+ class CmabFetchError < CmabError
203
+ # Exception raised when CMAB fetch fails
204
+
205
+ def initialize(msg = 'CMAB decision fetch failed with status:')
206
+ super
207
+ end
208
+ end
209
+
210
+ class CmabInvalidResponseError < CmabError
211
+ # Exception raised when CMAB fetch returns an invalid response
212
+
213
+ def initialize(msg = 'Invalid CMAB fetch response')
214
+ super
215
+ end
216
+ end
193
217
  end
@@ -201,6 +201,12 @@ module Optimizely
201
201
  },
202
202
  'forcedVariations' => {
203
203
  'type' => 'object'
204
+ },
205
+ 'cmab' => {
206
+ 'type' => 'object'
207
+ },
208
+ 'holdouts' => {
209
+ 'type' => 'array'
204
210
  }
205
211
  },
206
212
  'required' => %w[
@@ -303,6 +309,43 @@ module Optimizely
303
309
  },
304
310
  'required' => %w[key]
305
311
  }
312
+ },
313
+ 'cmab' => {
314
+ 'type' => 'object',
315
+ 'properties' => {
316
+ 'attributeIds' => {
317
+ 'type' => 'array',
318
+ 'items' => {'type' => 'string'}
319
+ },
320
+ 'trafficAllocation' => {
321
+ 'type' => 'integer'
322
+ }
323
+ }
324
+ },
325
+ 'holdouts' => {
326
+ 'type' => 'array',
327
+ 'items' => {
328
+ 'type' => 'object',
329
+ 'properties' => {
330
+ 'id' => {
331
+ 'type' => 'string'
332
+ },
333
+ 'key' => {
334
+ 'type' => 'string'
335
+ },
336
+ 'status' => {
337
+ 'type' => 'string'
338
+ },
339
+ 'includedFlags' => {
340
+ 'type' => 'array',
341
+ 'items' => {'type' => 'string'}
342
+ },
343
+ 'excludedFlags' => {
344
+ 'type' => 'array',
345
+ 'items' => {'type' => 'string'}
346
+ }
347
+ }
348
+ }
306
349
  }
307
350
  },
308
351
  'required' => %w[
@@ -454,6 +497,9 @@ module Optimizely
454
497
  'IF_MODIFIED_SINCE' => 'If-Modified-Since',
455
498
  'LAST_MODIFIED' => 'Last-Modified'
456
499
  }.freeze
500
+
501
+ CMAB_FETCH_FAILED = 'CMAB decision fetch failed (%s).'
502
+ INVALID_CMAB_FETCH_RESPONSE = 'Invalid CMAB fetch response'
457
503
  end
458
504
  end
459
505
  end
@@ -22,7 +22,7 @@ module Optimizely
22
22
  module Helpers
23
23
  class OptimizelySdkSettings
24
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
25
+ :odp_event_manager, :fetch_segments_timeout, :odp_event_timeout, :odp_flush_interval, :cmab_prediction_endpoint
26
26
 
27
27
  # Contains configuration used for Optimizely Project initialization.
28
28
  #
@@ -35,6 +35,7 @@ module Optimizely
35
35
  # @param odp_segment_request_timeout - Time to wait in seconds for fetch_qualified_segments (optional. default = 10).
36
36
  # @param odp_event_request_timeout - Time to wait in seconds for send_odp_events (optional. default = 10).
37
37
  # @param odp_event_flush_interval - Time to wait in seconds for odp events to accumulate before sending (optional. default = 1).
38
+ # @param cmab_prediction_endpoint - Custom CMAB prediction endpoint URL template (optional). Use %s as placeholder for rule_id. Defaults to production endpoint if not provided.
38
39
  def initialize(
39
40
  disable_odp: false,
40
41
  segments_cache_size: Constants::ODP_SEGMENTS_CACHE_CONFIG[:DEFAULT_CAPACITY],
@@ -44,7 +45,8 @@ module Optimizely
44
45
  odp_event_manager: nil,
45
46
  odp_segment_request_timeout: nil,
46
47
  odp_event_request_timeout: nil,
47
- odp_event_flush_interval: nil
48
+ odp_event_flush_interval: nil,
49
+ cmab_prediction_endpoint: nil
48
50
  )
49
51
  @odp_disabled = disable_odp
50
52
  @segments_cache_size = segments_cache_size
@@ -55,6 +57,7 @@ module Optimizely
55
57
  @fetch_segments_timeout = odp_segment_request_timeout
56
58
  @odp_event_timeout = odp_event_request_timeout
57
59
  @odp_flush_interval = odp_event_flush_interval
60
+ @cmab_prediction_endpoint = cmab_prediction_endpoint
58
61
  end
59
62
  end
60
63
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  #
4
- # Copyright 2022, Optimizely and contributors
4
+ # Copyright 2022-2025, 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.
@@ -91,6 +91,19 @@ module Optimizely
91
91
 
92
92
  @cache_mutex.synchronize { @map[key]&.value }
93
93
  end
94
+
95
+ # Remove the element associated with the provided key from the cache
96
+ #
97
+ # @param key - The key to remove
98
+
99
+ def remove(key)
100
+ return if @capacity <= 0
101
+
102
+ @cache_mutex.synchronize do
103
+ @map.delete(key)
104
+ end
105
+ nil
106
+ end
94
107
  end
95
108
 
96
109
  class CacheElement
@@ -22,6 +22,8 @@ require 'optimizely/event_dispatcher'
22
22
  require 'optimizely/event/batch_event_processor'
23
23
  require 'optimizely/logger'
24
24
  require 'optimizely/notification_center'
25
+ require 'optimizely/cmab/cmab_client'
26
+ require 'optimizely/cmab/cmab_service'
25
27
 
26
28
  module Optimizely
27
29
  class OptimizelyFactory
@@ -83,6 +85,46 @@ module Optimizely
83
85
  @blocking_timeout = blocking_timeout
84
86
  end
85
87
 
88
+ # Convenience method for setting CMAB cache size.
89
+ # @param cache_size Integer - Maximum number of items in CMAB cache.
90
+ # @param logger - Optional LoggerInterface Provides a log method to log messages.
91
+ def self.cmab_cache_size(cache_size, logger = NoOpLogger.new)
92
+ unless cache_size.is_a?(Integer) && cache_size.positive?
93
+ logger.log(
94
+ Logger::ERROR,
95
+ "CMAB cache size is invalid, setting to default size #{Optimizely::DefaultCmabCacheOptions::DEFAULT_CMAB_CACHE_SIZE}."
96
+ )
97
+ return
98
+ end
99
+ @cmab_cache_size = cache_size
100
+ end
101
+
102
+ # Convenience method for setting CMAB cache TTL.
103
+ # @param cache_ttl Numeric - Time in seconds for cache entries to live.
104
+ # @param logger - Optional LoggerInterface Provides a log method to log messages.
105
+ def self.cmab_cache_ttl(cache_ttl, logger = NoOpLogger.new)
106
+ unless cache_ttl.is_a?(Numeric) && cache_ttl.positive?
107
+ logger.log(
108
+ Logger::ERROR,
109
+ "CMAB cache TTL is invalid, setting to default TTL #{Optimizely::DefaultCmabCacheOptions::DEFAULT_CMAB_CACHE_TIMEOUT}."
110
+ )
111
+ return
112
+ end
113
+ @cmab_cache_ttl = cache_ttl
114
+ end
115
+
116
+ # Convenience method for setting custom CMAB cache.
117
+ # @param custom_cache - Cache implementation responding to lookup, save, remove, and reset methods.
118
+ def self.cmab_custom_cache(custom_cache)
119
+ @cmab_custom_cache = custom_cache
120
+ end
121
+
122
+ # Convenience method for setting custom CMAB prediction endpoint.
123
+ # @param prediction_endpoint String - Custom URL template for CMAB prediction API. Use %s as placeholder for rule_id.
124
+ def self.cmab_prediction_endpoint(prediction_endpoint)
125
+ @cmab_prediction_endpoint = prediction_endpoint
126
+ end
127
+
86
128
  # Returns a new optimizely instance.
87
129
  #
88
130
  # @params sdk_key - Required String uniquely identifying the fallback datafile corresponding to project.
@@ -142,7 +184,6 @@ module Optimizely
142
184
  notification_center = nil,
143
185
  settings = nil
144
186
  )
145
-
146
187
  error_handler ||= NoOpErrorHandler.new
147
188
  logger ||= NoOpLogger.new
148
189
  notification_center = notification_center.is_a?(Optimizely::NotificationCenter) ? notification_center : NotificationCenter.new(logger, error_handler)
@@ -166,6 +207,18 @@ module Optimizely
166
207
  notification_center: notification_center
167
208
  )
168
209
 
210
+ # Initialize CMAB components
211
+ cmab_prediction_endpoint = nil
212
+ cmab_prediction_endpoint = settings.cmab_prediction_endpoint if settings&.cmab_prediction_endpoint
213
+ cmab_prediction_endpoint ||= @cmab_prediction_endpoint
214
+
215
+ cmab_client = DefaultCmabClient.new(logger: logger, prediction_endpoint: cmab_prediction_endpoint)
216
+ cmab_cache = @cmab_custom_cache || LRUCache.new(
217
+ @cmab_cache_size || Optimizely::DefaultCmabCacheOptions::DEFAULT_CMAB_CACHE_SIZE,
218
+ @cmab_cache_ttl || Optimizely::DefaultCmabCacheOptions::DEFAULT_CMAB_CACHE_TIMEOUT
219
+ )
220
+ cmab_service = DefaultCmabService.new(cmab_cache, cmab_client, logger)
221
+
169
222
  Optimizely::Project.new(
170
223
  datafile: datafile,
171
224
  event_dispatcher: event_dispatcher,
@@ -177,7 +230,8 @@ module Optimizely
177
230
  config_manager: config_manager,
178
231
  notification_center: notification_center,
179
232
  event_processor: event_processor,
180
- settings: settings
233
+ settings: settings,
234
+ cmab_service: cmab_service
181
235
  )
182
236
  end
183
237
  end
@@ -62,6 +62,8 @@ module Optimizely
62
62
 
63
63
  def all_segments; end
64
64
 
65
+ def region; end
66
+
65
67
  def experiment_running?(experiment); end
66
68
 
67
69
  def get_experiment_from_key(experiment_key); end
@@ -86,6 +88,10 @@ module Optimizely
86
88
 
87
89
  def get_attribute_id(attribute_key); end
88
90
 
91
+ def get_attribute_by_key(attribute_key); end
92
+
93
+ def get_attribute_key_by_id(attribute_id); end
94
+
89
95
  def variation_id_exists?(experiment_id, variation_id); end
90
96
 
91
97
  def get_feature_flag_from_key(feature_flag_key); end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'logger'
4
+
5
+ module Optimizely
6
+ class UserProfileTracker
7
+ attr_reader :user_profile
8
+
9
+ def initialize(user_id, user_profile_service = nil, logger = nil)
10
+ @user_id = user_id
11
+ @user_profile_service = user_profile_service
12
+ @logger = logger || NoOpLogger.new
13
+ @profile_updated = false
14
+ @user_profile = {
15
+ user_id: user_id,
16
+ experiment_bucket_map: {}
17
+ }
18
+ end
19
+
20
+ def load_user_profile(reasons = [], error_handler = nil)
21
+ return if reasons.nil?
22
+
23
+ begin
24
+ @user_profile = @user_profile_service.lookup(@user_id) if @user_profile_service
25
+ if @user_profile.nil?
26
+ @user_profile = {
27
+ user_id: @user_id,
28
+ experiment_bucket_map: {}
29
+ }
30
+ end
31
+ rescue => e
32
+ message = "Error while looking up user profile for user ID '#{@user_id}': #{e}."
33
+ reasons << message
34
+ @logger.log(Logger::ERROR, message)
35
+ error_handler&.handle_error(e)
36
+ end
37
+ end
38
+
39
+ def update_user_profile(experiment_id, variation_id)
40
+ user_id = @user_profile[:user_id]
41
+ begin
42
+ @user_profile[:experiment_bucket_map][experiment_id] = {
43
+ variation_id: variation_id
44
+ }
45
+ @profile_updated = true
46
+ @logger.log(Logger::INFO, "Updated variation ID #{variation_id} of experiment ID #{experiment_id} for user '#{user_id}'.")
47
+ rescue => e
48
+ @logger.log(Logger::ERROR, "Error while updating user profile for user ID '#{user_id}': #{e}.")
49
+ end
50
+ end
51
+
52
+ def save_user_profile(error_handler = nil)
53
+ return unless @profile_updated && @user_profile_service
54
+
55
+ begin
56
+ @user_profile_service.save(@user_profile)
57
+ @logger.log(Logger::INFO, "Saved user profile for user '#{@user_profile[:user_id]}'.")
58
+ rescue => e
59
+ @logger.log(Logger::ERROR, "Failed to save user profile for user '#{@user_profile[:user_id]}': #{e}.")
60
+ error_handler&.handle_error(e)
61
+ end
62
+ end
63
+ end
64
+ end
@@ -17,5 +17,5 @@
17
17
  #
18
18
  module Optimizely
19
19
  CLIENT_ENGINE = 'ruby-sdk'
20
- VERSION = '5.0.1'
20
+ VERSION = '5.2.0'
21
21
  end