optimizely-sdk 5.2.0 → 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 +59 -47
- data/lib/optimizely/decision_service.rb +12 -13
- 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
- data/lib/optimizely.rb +1 -1
- metadata +3 -3
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,32 +114,14 @@ 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'
|
|
125
120
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if holdout['includedFlags'].nil? || holdout['includedFlags'].empty?
|
|
129
|
-
@global_holdouts[holdout['id']] = holdout
|
|
121
|
+
# Ensure holdout has layerId field (holdouts don't have campaigns)
|
|
122
|
+
holdout['layerId'] ||= ''
|
|
130
123
|
|
|
131
|
-
|
|
132
|
-
if excluded_flags && !excluded_flags.empty?
|
|
133
|
-
excluded_flags.each do |flag_id|
|
|
134
|
-
@excluded_holdouts[flag_id] ||= []
|
|
135
|
-
@excluded_holdouts[flag_id] << holdout
|
|
136
|
-
end
|
|
137
|
-
end
|
|
138
|
-
else
|
|
139
|
-
holdout['includedFlags'].each do |flag_id|
|
|
140
|
-
@included_holdouts[flag_id] ||= []
|
|
141
|
-
@included_holdouts[flag_id] << holdout
|
|
142
|
-
end
|
|
143
|
-
end
|
|
124
|
+
@holdout_id_map[holdout['id']] = holdout
|
|
144
125
|
end
|
|
145
126
|
|
|
146
127
|
@experiment_id_map.each_value do |exp|
|
|
@@ -169,7 +150,6 @@ module Optimizely
|
|
|
169
150
|
@all_segments.concat Audience.get_segments(audience['conditions'])
|
|
170
151
|
end
|
|
171
152
|
|
|
172
|
-
@flag_variation_map = generate_feature_variation_map(@feature_flags)
|
|
173
153
|
@all_experiments = @experiment_id_map.merge(@rollout_experiment_id_map)
|
|
174
154
|
@all_experiments.each do |id, exp|
|
|
175
155
|
variations = exp.fetch('variations')
|
|
@@ -195,19 +175,32 @@ module Optimizely
|
|
|
195
175
|
@experiment_feature_map[experiment_id] = [feature_flag['id']]
|
|
196
176
|
end
|
|
197
177
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
|
206
198
|
end
|
|
207
|
-
|
|
208
|
-
@flag_holdouts_map[key] = applicable_holdouts unless applicable_holdouts.empty?
|
|
209
199
|
end
|
|
210
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
|
+
|
|
211
204
|
# Adding Holdout variations in variation id and key maps
|
|
212
205
|
return unless @holdouts && !@holdouts.empty?
|
|
213
206
|
|
|
@@ -635,19 +628,6 @@ module Optimizely
|
|
|
635
628
|
@rollout_experiment_id_map.key?(experiment_id)
|
|
636
629
|
end
|
|
637
630
|
|
|
638
|
-
def get_holdouts_for_flag(flag_id)
|
|
639
|
-
# Helper method to get holdouts from an applied feature flag
|
|
640
|
-
#
|
|
641
|
-
# flag_id - (REQUIRED) ID of the feature flag
|
|
642
|
-
# This parameter is required and should not be null/nil
|
|
643
|
-
#
|
|
644
|
-
# Returns the holdouts that apply for a specific flag
|
|
645
|
-
|
|
646
|
-
return [] if @holdouts.nil? || @holdouts.empty?
|
|
647
|
-
|
|
648
|
-
@flag_holdouts_map[flag_id] || []
|
|
649
|
-
end
|
|
650
|
-
|
|
651
631
|
def get_holdout(holdout_id)
|
|
652
632
|
# Helper method to get holdout from holdout ID
|
|
653
633
|
#
|
|
@@ -664,6 +644,38 @@ module Optimizely
|
|
|
664
644
|
|
|
665
645
|
private
|
|
666
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
|
+
|
|
667
679
|
def generate_feature_variation_map(feature_flags)
|
|
668
680
|
flag_variation_map = {}
|
|
669
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)
|
|
@@ -214,6 +217,9 @@ module Optimizely
|
|
|
214
217
|
|
|
215
218
|
return DecisionResult.new(experiment_decision.decision, experiment_decision.error, reasons) if experiment_decision.decision
|
|
216
219
|
|
|
220
|
+
# If there's an error (e.g., CMAB error), return immediately without falling back to rollout
|
|
221
|
+
return DecisionResult.new(nil, experiment_decision.error, reasons) if experiment_decision.error
|
|
222
|
+
|
|
217
223
|
# Check if the feature flag has a rollout and the user is bucketed into that rollout
|
|
218
224
|
rollout_decision = get_variation_for_feature_rollout(project_config, feature_flag, user_context)
|
|
219
225
|
reasons.push(*rollout_decision.reasons)
|
|
@@ -312,15 +318,8 @@ module Optimizely
|
|
|
312
318
|
|
|
313
319
|
decisions = []
|
|
314
320
|
feature_flags.each do |feature_flag|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
# Only process rollout if no experiment decision was found and no error
|
|
318
|
-
if decision_result.decision.nil? && !decision_result.error
|
|
319
|
-
decision_result_rollout = get_variation_for_feature_rollout(project_config, feature_flag, user_context) unless decision_result.decision
|
|
320
|
-
decision_result.decision = decision_result_rollout.decision
|
|
321
|
-
decision_result.reasons.push(*decision_result_rollout.reasons)
|
|
322
|
-
end
|
|
323
|
-
decisions << decision_result
|
|
321
|
+
decision = get_decision_for_flag(feature_flag, user_context, project_config, decide_options, user_profile_tracker)
|
|
322
|
+
decisions << decision
|
|
324
323
|
end
|
|
325
324
|
user_profile_tracker&.save_user_profile
|
|
326
325
|
decisions
|
|
@@ -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
data/lib/optimizely.rb
CHANGED
|
@@ -220,7 +220,7 @@ module Optimizely
|
|
|
220
220
|
decision_source = decision.source
|
|
221
221
|
end
|
|
222
222
|
|
|
223
|
-
if !decide_options.include?(OptimizelyDecideOption::DISABLE_DECISION_EVENT) && (decision_source == Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST'] || config.send_flag_decisions)
|
|
223
|
+
if !decide_options.include?(OptimizelyDecideOption::DISABLE_DECISION_EVENT) && (decision_source == Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST'] || decision_source == Optimizely::DecisionService::DECISION_SOURCES['HOLDOUT'] || config.send_flag_decisions)
|
|
224
224
|
send_impression(config, experiment, variation_key || '', flag_key, rule_key || '', feature_enabled, decision_source, user_id, attributes, decision&.cmab_uuid)
|
|
225
225
|
decision_event_dispatched = true
|
|
226
226
|
end
|
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
|
|
@@ -216,7 +216,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
216
216
|
- !ruby/object:Gem::Version
|
|
217
217
|
version: '0'
|
|
218
218
|
requirements: []
|
|
219
|
-
rubygems_version: 3.4.
|
|
219
|
+
rubygems_version: 3.4.10
|
|
220
220
|
signing_key:
|
|
221
221
|
specification_version: 4
|
|
222
222
|
summary: Ruby SDK for Optimizely's testing framework
|