optimizely-sdk 5.2.1 → 5.3.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/config/datafile_project_config.rb +58 -72
- data/lib/optimizely/decision_service.rb +7 -4
- data/lib/optimizely/event/batch_event_processor.rb +39 -13
- data/lib/optimizely/helpers/constants.rb +18 -10
- data/lib/optimizely/odp/odp_event_manager.rb +17 -1
- data/lib/optimizely/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 98f3ee93c9d69f15110623bfffad642326cd891607899ded857c6289e315a34f
|
|
4
|
+
data.tar.gz: ee719a38656735b49e65d37a261b4b709a84a76d9157d40889ce7be4a56c0029
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 898908417c5e73e4136295ac92c942597cce81be13b6f2def6871c0635cf4c5ac52731140a0eaf154a6cfe75e3819e0fe7061f0d3af9c039e82e8a6ff9853592
|
|
7
|
+
data.tar.gz: 9938341c76b893da676e2bbcfd7406959ead3b38558bf8052261f50acde9d42f4e556af3e61cf84fbc6a8c34abf2c15bf69ce0812ca91ea9c7249a10b30bcdde
|
|
@@ -33,8 +33,7 @@ module Optimizely
|
|
|
33
33
|
:group_id_map, :rollout_id_map, :rollout_experiment_id_map, :variation_id_map,
|
|
34
34
|
:variation_id_to_variable_usage_map, :variation_key_map, :variation_id_map_by_experiment_id,
|
|
35
35
|
:variation_key_map_by_experiment_id, :flag_variation_map, :integration_key_map, :integrations,
|
|
36
|
-
:public_key_for_odp, :host_for_odp, :all_segments, :region, :holdouts, :holdout_id_map
|
|
37
|
-
:global_holdouts, :included_holdouts, :excluded_holdouts, :flag_holdouts_map
|
|
36
|
+
:public_key_for_odp, :host_for_odp, :all_segments, :region, :holdouts, :holdout_id_map
|
|
38
37
|
# Boolean - denotes if Optimizely should remove the last block of visitors' IP address before storing event data
|
|
39
38
|
attr_reader :anonymize_ip
|
|
40
39
|
|
|
@@ -115,10 +114,6 @@ module Optimizely
|
|
|
115
114
|
@variation_id_to_experiment_map = {}
|
|
116
115
|
@flag_variation_map = {}
|
|
117
116
|
@holdout_id_map = {}
|
|
118
|
-
@global_holdouts = []
|
|
119
|
-
@included_holdouts = {}
|
|
120
|
-
@excluded_holdouts = {}
|
|
121
|
-
@flag_holdouts_map = {}
|
|
122
117
|
|
|
123
118
|
@holdouts.each do |holdout|
|
|
124
119
|
next unless holdout['status'] == 'Running'
|
|
@@ -127,31 +122,6 @@ module Optimizely
|
|
|
127
122
|
holdout['layerId'] ||= ''
|
|
128
123
|
|
|
129
124
|
@holdout_id_map[holdout['id']] = holdout
|
|
130
|
-
|
|
131
|
-
included_flags = holdout['includedFlags'] || []
|
|
132
|
-
excluded_flags = holdout['excludedFlags'] || []
|
|
133
|
-
|
|
134
|
-
case [included_flags.empty?, excluded_flags.empty?]
|
|
135
|
-
when [true, true]
|
|
136
|
-
# No included or excluded flags - this is a global holdout
|
|
137
|
-
@global_holdouts << holdout
|
|
138
|
-
|
|
139
|
-
when [false, true], [false, false]
|
|
140
|
-
# Has included flags - add to included_holdouts map
|
|
141
|
-
included_flags.each do |flag_id|
|
|
142
|
-
@included_holdouts[flag_id] ||= []
|
|
143
|
-
@included_holdouts[flag_id] << holdout
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
when [true, false]
|
|
147
|
-
# No included flags but has excluded flags - global with exclusions
|
|
148
|
-
@global_holdouts << holdout
|
|
149
|
-
|
|
150
|
-
excluded_flags.each do |flag_id|
|
|
151
|
-
@excluded_holdouts[flag_id] ||= []
|
|
152
|
-
@excluded_holdouts[flag_id] << holdout
|
|
153
|
-
end
|
|
154
|
-
end
|
|
155
125
|
end
|
|
156
126
|
|
|
157
127
|
@experiment_id_map.each_value do |exp|
|
|
@@ -180,7 +150,6 @@ module Optimizely
|
|
|
180
150
|
@all_segments.concat Audience.get_segments(audience['conditions'])
|
|
181
151
|
end
|
|
182
152
|
|
|
183
|
-
@flag_variation_map = generate_feature_variation_map(@feature_flags)
|
|
184
153
|
@all_experiments = @experiment_id_map.merge(@rollout_experiment_id_map)
|
|
185
154
|
@all_experiments.each do |id, exp|
|
|
186
155
|
variations = exp.fetch('variations')
|
|
@@ -205,8 +174,33 @@ module Optimizely
|
|
|
205
174
|
feature_flag['experimentIds'].each do |experiment_id|
|
|
206
175
|
@experiment_feature_map[experiment_id] = [feature_flag['id']]
|
|
207
176
|
end
|
|
177
|
+
|
|
178
|
+
# Feature Rollout support: inject the "everyone else" variation
|
|
179
|
+
# into any experiment with type == "feature_rollout"
|
|
180
|
+
everyone_else_variation = get_everyone_else_variation(feature_flag)
|
|
181
|
+
next if everyone_else_variation.nil?
|
|
182
|
+
|
|
183
|
+
feature_flag['experimentIds'].each do |exp_id|
|
|
184
|
+
experiment = @experiment_id_map[exp_id]
|
|
185
|
+
next unless experiment && experiment['type'] == Helpers::Constants::EXPERIMENT_TYPES['fr']
|
|
186
|
+
|
|
187
|
+
experiment['variations'].push(everyone_else_variation)
|
|
188
|
+
experiment['trafficAllocation'].push(
|
|
189
|
+
'entityId' => everyone_else_variation['id'],
|
|
190
|
+
'endOfRange' => 10_000
|
|
191
|
+
)
|
|
192
|
+
@variation_key_map[experiment['key']][everyone_else_variation['key']] = everyone_else_variation
|
|
193
|
+
@variation_id_map[experiment['key']][everyone_else_variation['id']] = everyone_else_variation
|
|
194
|
+
@variation_id_map_by_experiment_id[exp_id][everyone_else_variation['id']] = everyone_else_variation
|
|
195
|
+
@variation_key_map_by_experiment_id[exp_id][everyone_else_variation['key']] = everyone_else_variation
|
|
196
|
+
variation_variables = everyone_else_variation['variables']
|
|
197
|
+
@variation_id_to_variable_usage_map[everyone_else_variation['id']] = generate_key_map(variation_variables, 'id') if variation_variables
|
|
198
|
+
end
|
|
208
199
|
end
|
|
209
200
|
|
|
201
|
+
# Generate flag_variation_map after injection so it includes everyone-else variations
|
|
202
|
+
@flag_variation_map = generate_feature_variation_map(@feature_flags)
|
|
203
|
+
|
|
210
204
|
# Adding Holdout variations in variation id and key maps
|
|
211
205
|
return unless @holdouts && !@holdouts.empty?
|
|
212
206
|
|
|
@@ -634,46 +628,6 @@ module Optimizely
|
|
|
634
628
|
@rollout_experiment_id_map.key?(experiment_id)
|
|
635
629
|
end
|
|
636
630
|
|
|
637
|
-
def get_holdouts_for_flag(flag_id)
|
|
638
|
-
# Helper method to get holdouts from an applied feature flag
|
|
639
|
-
#
|
|
640
|
-
# flag_id - (REQUIRED) ID of the feature flag
|
|
641
|
-
# This parameter is required and should not be null/nil
|
|
642
|
-
#
|
|
643
|
-
# Returns the holdouts that apply for a specific flag
|
|
644
|
-
|
|
645
|
-
return [] if @holdouts.nil? || @holdouts.empty?
|
|
646
|
-
|
|
647
|
-
# Check cache first (before validation, so we cache the validation result too)
|
|
648
|
-
return @flag_holdouts_map[flag_id] if @flag_holdouts_map.key?(flag_id)
|
|
649
|
-
|
|
650
|
-
# Validate that the flag exists in the datafile
|
|
651
|
-
flag_exists = @feature_flags.any? { |flag| flag['id'] == flag_id }
|
|
652
|
-
unless flag_exists
|
|
653
|
-
# Cache the empty result for non-existent flags
|
|
654
|
-
@flag_holdouts_map[flag_id] = []
|
|
655
|
-
return []
|
|
656
|
-
end
|
|
657
|
-
|
|
658
|
-
# Prioritize global holdouts first
|
|
659
|
-
excluded = @excluded_holdouts[flag_id] || []
|
|
660
|
-
|
|
661
|
-
active_holdouts = if excluded.any?
|
|
662
|
-
@global_holdouts.reject { |holdout| excluded.include?(holdout) }
|
|
663
|
-
else
|
|
664
|
-
@global_holdouts.dup
|
|
665
|
-
end
|
|
666
|
-
|
|
667
|
-
# Append included holdouts
|
|
668
|
-
included = @included_holdouts[flag_id] || []
|
|
669
|
-
active_holdouts.concat(included)
|
|
670
|
-
|
|
671
|
-
# Cache the result
|
|
672
|
-
@flag_holdouts_map[flag_id] = active_holdouts
|
|
673
|
-
|
|
674
|
-
@flag_holdouts_map[flag_id] || []
|
|
675
|
-
end
|
|
676
|
-
|
|
677
631
|
def get_holdout(holdout_id)
|
|
678
632
|
# Helper method to get holdout from holdout ID
|
|
679
633
|
#
|
|
@@ -690,6 +644,38 @@ module Optimizely
|
|
|
690
644
|
|
|
691
645
|
private
|
|
692
646
|
|
|
647
|
+
def get_everyone_else_variation(feature_flag)
|
|
648
|
+
# Get the "everyone else" variation for a feature flag.
|
|
649
|
+
#
|
|
650
|
+
# The "everyone else" rule is the last experiment in the flag's rollout,
|
|
651
|
+
# and its first variation is the "everyone else" variation.
|
|
652
|
+
#
|
|
653
|
+
# feature_flag - Feature flag hash
|
|
654
|
+
#
|
|
655
|
+
# Returns the "everyone else" variation hash, or nil if not available.
|
|
656
|
+
|
|
657
|
+
rollout_id = feature_flag['rolloutId']
|
|
658
|
+
return nil if rollout_id.nil? || rollout_id.empty?
|
|
659
|
+
|
|
660
|
+
rollout = @rollout_id_map[rollout_id]
|
|
661
|
+
return nil if rollout.nil?
|
|
662
|
+
|
|
663
|
+
experiments = rollout['experiments']
|
|
664
|
+
return nil if experiments.nil? || experiments.empty?
|
|
665
|
+
|
|
666
|
+
everyone_else_rule = experiments.last
|
|
667
|
+
variations = everyone_else_rule['variations']
|
|
668
|
+
return nil if variations.nil? || variations.empty?
|
|
669
|
+
|
|
670
|
+
variation = variations.first
|
|
671
|
+
{
|
|
672
|
+
'id' => variation['id'],
|
|
673
|
+
'key' => variation['key'],
|
|
674
|
+
'featureEnabled' => variation['featureEnabled'] == true,
|
|
675
|
+
'variables' => variation.fetch('variables', [])
|
|
676
|
+
}
|
|
677
|
+
end
|
|
678
|
+
|
|
693
679
|
def generate_feature_variation_map(feature_flags)
|
|
694
680
|
flag_variation_map = {}
|
|
695
681
|
feature_flags.each do |flag|
|
|
@@ -132,6 +132,8 @@ module Optimizely
|
|
|
132
132
|
return VariationResult.new(nil, true, decide_reasons, nil)
|
|
133
133
|
end
|
|
134
134
|
|
|
135
|
+
@logger.log(Logger::DEBUG, "Skipping user profile service for CMAB experiment '#{experiment_key}'. CMAB decisions are dynamic and not stored for sticky bucketing.")
|
|
136
|
+
should_ignore_user_profile_service = true
|
|
135
137
|
cmab_decision = cmab_decision_result.result
|
|
136
138
|
variation_id = cmab_decision&.variation_id
|
|
137
139
|
cmab_uuid = cmab_decision&.cmab_uuid
|
|
@@ -167,9 +169,10 @@ module Optimizely
|
|
|
167
169
|
# user_context - Optimizely user context instance
|
|
168
170
|
#
|
|
169
171
|
# Returns DecisionResult struct.
|
|
170
|
-
holdouts
|
|
172
|
+
# Get running holdouts from the holdout_id_map (all holdouts are global now)
|
|
173
|
+
running_holdouts = project_config.holdout_id_map.values
|
|
171
174
|
|
|
172
|
-
if
|
|
175
|
+
if running_holdouts && !running_holdouts.empty?
|
|
173
176
|
# Has holdouts - use get_decision_for_flag which checks holdouts first
|
|
174
177
|
get_decision_for_flag(feature_flag, user_context, project_config, decide_options)
|
|
175
178
|
else
|
|
@@ -193,8 +196,8 @@ module Optimizely
|
|
|
193
196
|
reasons = decide_reasons ? decide_reasons.dup : []
|
|
194
197
|
user_id = user_context.user_id
|
|
195
198
|
|
|
196
|
-
# Check holdouts
|
|
197
|
-
holdouts = project_config.
|
|
199
|
+
# Check holdouts (all holdouts are global now - apply to all flags)
|
|
200
|
+
holdouts = project_config.holdout_id_map.values
|
|
198
201
|
|
|
199
202
|
holdouts.each do |holdout|
|
|
200
203
|
holdout_decision = get_variation_for_holdout(holdout, user_context, project_config)
|
|
@@ -172,20 +172,35 @@ module Optimizely
|
|
|
172
172
|
return if @current_batch.empty?
|
|
173
173
|
|
|
174
174
|
log_event = Optimizely::EventFactory.create_log_event(@current_batch, @logger)
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
175
|
+
@logger.log(
|
|
176
|
+
Logger::INFO,
|
|
177
|
+
'Flushing Queue.'
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
retry_count = 0
|
|
181
|
+
max_retries = Optimizely::Helpers::Constants::EVENT_DISPATCH_CONFIG[:MAX_RETRIES]
|
|
182
|
+
|
|
183
|
+
while retry_count < max_retries
|
|
184
|
+
begin
|
|
185
|
+
@event_dispatcher.dispatch_event(log_event)
|
|
186
|
+
@notification_center&.send_notifications(
|
|
187
|
+
NotificationCenter::NOTIFICATION_TYPES[:LOG_EVENT],
|
|
188
|
+
log_event
|
|
189
|
+
)
|
|
190
|
+
# Success - break out of retry loop
|
|
191
|
+
break
|
|
192
|
+
rescue StandardError => e
|
|
193
|
+
@logger.log(Logger::ERROR, "Error dispatching event: #{log_event} #{e.message}.")
|
|
194
|
+
retry_count += 1
|
|
195
|
+
|
|
196
|
+
if retry_count < max_retries
|
|
197
|
+
delay = calculate_retry_interval(retry_count - 1)
|
|
198
|
+
@logger.log(Logger::DEBUG, "Retrying event dispatch (attempt #{retry_count + 1} of #{max_retries}) after #{delay}s")
|
|
199
|
+
sleep(delay)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
188
202
|
end
|
|
203
|
+
|
|
189
204
|
@current_batch = []
|
|
190
205
|
end
|
|
191
206
|
|
|
@@ -231,5 +246,16 @@ module Optimizely
|
|
|
231
246
|
# false otherwise.
|
|
232
247
|
Helpers::Validator.finite_number?(value) && value.positive?
|
|
233
248
|
end
|
|
249
|
+
|
|
250
|
+
# Calculate exponential backoff interval: 200ms, 400ms, 800ms, ... capped at 1s
|
|
251
|
+
#
|
|
252
|
+
# @param retry_count - Zero-based retry count
|
|
253
|
+
# @return [Float] - Delay in seconds
|
|
254
|
+
def calculate_retry_interval(retry_count)
|
|
255
|
+
initial_interval = Helpers::Constants::EVENT_DISPATCH_CONFIG[:INITIAL_RETRY_INTERVAL]
|
|
256
|
+
max_interval = Helpers::Constants::EVENT_DISPATCH_CONFIG[:MAX_RETRY_INTERVAL]
|
|
257
|
+
interval = initial_interval * (2**retry_count)
|
|
258
|
+
[interval, max_interval].min
|
|
259
|
+
end
|
|
234
260
|
end
|
|
235
261
|
end
|
|
@@ -18,6 +18,14 @@
|
|
|
18
18
|
module Optimizely
|
|
19
19
|
module Helpers
|
|
20
20
|
module Constants
|
|
21
|
+
EXPERIMENT_TYPES = {
|
|
22
|
+
'ab' => 'ab',
|
|
23
|
+
'mab' => 'mab',
|
|
24
|
+
'cmab' => 'cmab',
|
|
25
|
+
'td' => 'td',
|
|
26
|
+
'fr' => 'fr'
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
21
29
|
JSON_SCHEMA_V2 = {
|
|
22
30
|
'type' => 'object',
|
|
23
31
|
'properties' => {
|
|
@@ -205,6 +213,9 @@ module Optimizely
|
|
|
205
213
|
'cmab' => {
|
|
206
214
|
'type' => 'object'
|
|
207
215
|
},
|
|
216
|
+
'type' => {
|
|
217
|
+
'type' => %w[string null]
|
|
218
|
+
},
|
|
208
219
|
'holdouts' => {
|
|
209
220
|
'type' => 'array'
|
|
210
221
|
}
|
|
@@ -335,14 +346,6 @@ module Optimizely
|
|
|
335
346
|
},
|
|
336
347
|
'status' => {
|
|
337
348
|
'type' => 'string'
|
|
338
|
-
},
|
|
339
|
-
'includedFlags' => {
|
|
340
|
-
'type' => 'array',
|
|
341
|
-
'items' => {'type' => 'string'}
|
|
342
|
-
},
|
|
343
|
-
'excludedFlags' => {
|
|
344
|
-
'type' => 'array',
|
|
345
|
-
'items' => {'type' => 'string'}
|
|
346
349
|
}
|
|
347
350
|
}
|
|
348
351
|
}
|
|
@@ -459,7 +462,10 @@ module Optimizely
|
|
|
459
462
|
}.freeze
|
|
460
463
|
|
|
461
464
|
EVENT_DISPATCH_CONFIG = {
|
|
462
|
-
REQUEST_TIMEOUT: 10
|
|
465
|
+
REQUEST_TIMEOUT: 10,
|
|
466
|
+
MAX_RETRIES: 3,
|
|
467
|
+
INITIAL_RETRY_INTERVAL: 0.2, # 200ms in seconds
|
|
468
|
+
MAX_RETRY_INTERVAL: 1.0 # 1 second
|
|
463
469
|
}.freeze
|
|
464
470
|
|
|
465
471
|
ODP_GRAPHQL_API_CONFIG = {
|
|
@@ -490,7 +496,9 @@ module Optimizely
|
|
|
490
496
|
DEFAULT_QUEUE_CAPACITY: 10_000,
|
|
491
497
|
DEFAULT_BATCH_SIZE: 10,
|
|
492
498
|
DEFAULT_FLUSH_INTERVAL_SECONDS: 1,
|
|
493
|
-
DEFAULT_RETRY_COUNT: 3
|
|
499
|
+
DEFAULT_RETRY_COUNT: 3,
|
|
500
|
+
INITIAL_RETRY_INTERVAL: 0.2, # 200ms in seconds
|
|
501
|
+
MAX_RETRY_INTERVAL: 1.0 # 1 second
|
|
494
502
|
}.freeze
|
|
495
503
|
|
|
496
504
|
HTTP_HEADERS = {
|
|
@@ -239,7 +239,12 @@ module Optimizely
|
|
|
239
239
|
end
|
|
240
240
|
break unless should_retry
|
|
241
241
|
|
|
242
|
-
|
|
242
|
+
if i < @retry_count - 1
|
|
243
|
+
# Exponential backoff: 200ms, 400ms, 800ms, ... capped at 1s
|
|
244
|
+
delay = calculate_retry_interval(i)
|
|
245
|
+
@logger.log(Logger::DEBUG, "Error dispatching ODP events, retrying (attempt #{i + 2} of #{@retry_count}) after #{delay}s")
|
|
246
|
+
sleep(delay)
|
|
247
|
+
end
|
|
243
248
|
i += 1
|
|
244
249
|
end
|
|
245
250
|
|
|
@@ -282,5 +287,16 @@ module Optimizely
|
|
|
282
287
|
@api_key = @odp_config&.api_key
|
|
283
288
|
@api_host = @odp_config&.api_host
|
|
284
289
|
end
|
|
290
|
+
|
|
291
|
+
# Calculate exponential backoff interval: 200ms, 400ms, 800ms, ... capped at 1s
|
|
292
|
+
#
|
|
293
|
+
# @param retry_count - Zero-based retry count
|
|
294
|
+
# @return [Float] - Delay in seconds
|
|
295
|
+
def calculate_retry_interval(retry_count)
|
|
296
|
+
initial_interval = Helpers::Constants::ODP_EVENT_MANAGER[:INITIAL_RETRY_INTERVAL]
|
|
297
|
+
max_interval = Helpers::Constants::ODP_EVENT_MANAGER[:MAX_RETRY_INTERVAL]
|
|
298
|
+
interval = initial_interval * (2**retry_count)
|
|
299
|
+
[interval, max_interval].min
|
|
300
|
+
end
|
|
285
301
|
end
|
|
286
302
|
end
|
data/lib/optimizely/version.rb
CHANGED
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: 5.
|
|
4
|
+
version: 5.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Optimizely
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-05-05 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bundler
|