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.
- checksums.yaml +4 -4
- data/lib/optimizely/audience.rb +15 -1
- data/lib/optimizely/bucketer.rb +34 -13
- data/lib/optimizely/cmab/cmab_client.rb +230 -0
- data/lib/optimizely/cmab/cmab_service.rb +218 -0
- data/lib/optimizely/config/datafile_project_config.rb +140 -2
- data/lib/optimizely/config_manager/http_project_config_manager.rb +1 -1
- data/lib/optimizely/decide/optimizely_decide_option.rb +3 -0
- data/lib/optimizely/decide/optimizely_decision.rb +19 -0
- data/lib/optimizely/decision_service.rb +280 -59
- data/lib/optimizely/event/entity/event_context.rb +5 -2
- data/lib/optimizely/event/event_factory.rb +8 -2
- data/lib/optimizely/event/user_event_factory.rb +2 -0
- data/lib/optimizely/event_builder.rb +15 -5
- data/lib/optimizely/exceptions.rb +24 -0
- data/lib/optimizely/helpers/constants.rb +46 -0
- data/lib/optimizely/helpers/sdk_settings.rb +5 -2
- data/lib/optimizely/odp/lru_cache.rb +14 -1
- data/lib/optimizely/optimizely_factory.rb +56 -2
- data/lib/optimizely/project_config.rb +6 -0
- data/lib/optimizely/user_profile_tracker.rb +64 -0
- data/lib/optimizely/version.rb +1 -1
- data/lib/optimizely.rb +173 -70
- metadata +6 -3
|
@@ -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
|
data/lib/optimizely/version.rb
CHANGED