optimizely-sdk 5.0.1 → 5.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 28a736eab23780d594a9fbe207de49b685681d1d1ff1e8aea5f79119d8688728
4
- data.tar.gz: d2eea43d2d2b2bebc0ba80d62388434fc67c111531c26ad0aa63dd8ba350a798
3
+ metadata.gz: 79f6fce62ba26147eef50a92fefb2cb443df50b2b3a0f4c15f75352f4052aaa8
4
+ data.tar.gz: d09bfd23ad927fa5ae62c03467647c65bc21ac68d19d88dc81ef6dea441a25bf
5
5
  SHA512:
6
- metadata.gz: f1afde6d790fa44cc8cc1aedf4000aee59fa1ad27152b692eacc127d0993ef9c596f3b607c1df4c430ab98f86e9409af99106c5d9292d8702c5258944dd7989d
7
- data.tar.gz: edf53d55f80565197ba3a10a436b455701a38400f3f1e2d95e63418a7b777ff9059c39a479ffd23a09658661f74af9d6a36b0860da1be919d06781a106d218a1
6
+ metadata.gz: 74e1e587ffdcadfcbbea0677d0de60376e7f677a8254d1e121d37994301c5ebd1959e21ff7bfea1493a441835a20e135dc4d4a4f1577939772b6e167d35f2789
7
+ data.tar.gz: 57c17f7508daeaed88755b9dc4463f019fb5b5a523c32cf5639375c06a07dd563786001282e59496d5e8c149c5d884ea5c16522d52edef03c40a0b9ff1890879
@@ -52,17 +52,20 @@ module Optimizely
52
52
  @forced_variation_map = {}
53
53
  end
54
54
 
55
- def get_variation(project_config, experiment_id, user_context, decide_options = [])
55
+ def get_variation(project_config, experiment_id, user_context, user_profile_tracker = nil, decide_options = [], reasons = [])
56
56
  # Determines variation into which user will be bucketed.
57
57
  #
58
58
  # project_config - project_config - Instance of ProjectConfig
59
59
  # experiment_id - Experiment for which visitor variation needs to be determined
60
60
  # user_context - Optimizely user context instance
61
+ # user_profile_tracker: Tracker for reading and updating user profile of the user.
62
+ # reasons: Decision reasons.
61
63
  #
62
64
  # Returns variation ID where visitor will be bucketed
63
65
  # (nil if experiment is inactive or user does not meet audience conditions)
64
-
66
+ user_profile_tracker = UserProfileTracker.new(user_context.user_id, @user_profile_service, @logger) unless user_profile_tracker.is_a?(Optimizely::UserProfileTracker)
65
67
  decide_reasons = []
68
+ decide_reasons.push(*reasons)
66
69
  user_id = user_context.user_id
67
70
  attributes = user_context.user_attributes
68
71
  # By default, the bucketing ID should be the user ID
@@ -92,10 +95,8 @@ module Optimizely
92
95
 
93
96
  should_ignore_user_profile_service = decide_options.include? Optimizely::Decide::OptimizelyDecideOption::IGNORE_USER_PROFILE_SERVICE
94
97
  # Check for saved bucketing decisions if decide_options do not include ignoreUserProfileService
95
- unless should_ignore_user_profile_service
96
- user_profile, reasons_received = get_user_profile(user_id)
97
- decide_reasons.push(*reasons_received)
98
- saved_variation_id, reasons_received = get_saved_variation_id(project_config, experiment_id, user_profile)
98
+ unless should_ignore_user_profile_service && user_profile_tracker
99
+ saved_variation_id, reasons_received = get_saved_variation_id(project_config, experiment_id, user_profile_tracker.user_profile)
99
100
  decide_reasons.push(*reasons_received)
100
101
  if saved_variation_id
101
102
  message = "Returning previously activated variation ID #{saved_variation_id} of experiment '#{experiment_key}' for user '#{user_id}' from user profile."
@@ -131,7 +132,7 @@ module Optimizely
131
132
  decide_reasons.push(message)
132
133
 
133
134
  # Persist bucketing decision
134
- save_user_profile(user_profile, experiment_id, variation_id) unless should_ignore_user_profile_service
135
+ user_profile_tracker.update_user_profile(experiment_id, variation_id) unless should_ignore_user_profile_service && user_profile_tracker
135
136
  [variation_id, decide_reasons]
136
137
  end
137
138
 
@@ -143,21 +144,46 @@ module Optimizely
143
144
  # user_context - Optimizely user context instance
144
145
  #
145
146
  # Returns Decision struct (nil if the user is not bucketed into any of the experiments on the feature)
147
+ get_variations_for_feature_list(project_config, [feature_flag], user_context, decide_options).first
148
+ end
146
149
 
147
- decide_reasons = []
148
-
149
- # check if the feature is being experiment on and whether the user is bucketed into the experiment
150
- decision, reasons_received = get_variation_for_feature_experiment(project_config, feature_flag, user_context, decide_options)
151
- decide_reasons.push(*reasons_received)
152
- return decision, decide_reasons unless decision.nil?
153
-
154
- decision, reasons_received = get_variation_for_feature_rollout(project_config, feature_flag, user_context)
155
- decide_reasons.push(*reasons_received)
156
-
157
- [decision, decide_reasons]
150
+ def get_variations_for_feature_list(project_config, feature_flags, user_context, decide_options = [])
151
+ # Returns the list of experiment/variation the user is bucketed in for the given list of features.
152
+ #
153
+ # Args:
154
+ # project_config: Instance of ProjectConfig.
155
+ # feature_flags: Array of features for which we are determining if it is enabled or not for the given user.
156
+ # user_context: User context for user.
157
+ # decide_options: Decide options.
158
+ #
159
+ # Returns:
160
+ # Array of Decision struct.
161
+ ignore_ups = decide_options.include? Optimizely::Decide::OptimizelyDecideOption::IGNORE_USER_PROFILE_SERVICE
162
+ user_profile_tracker = nil
163
+ unless ignore_ups && @user_profile_service
164
+ user_profile_tracker = UserProfileTracker.new(user_context.user_id, @user_profile_service, @logger)
165
+ user_profile_tracker.load_user_profile
166
+ end
167
+ decisions = []
168
+ feature_flags.each do |feature_flag|
169
+ decide_reasons = []
170
+ # check if the feature is being experiment on and whether the user is bucketed into the experiment
171
+ decision, reasons_received = get_variation_for_feature_experiment(project_config, feature_flag, user_context, user_profile_tracker, decide_options)
172
+ decide_reasons.push(*reasons_received)
173
+ if decision
174
+ decisions << [decision, decide_reasons]
175
+ else
176
+ # Proceed to rollout if the decision is nil
177
+ rollout_decision, reasons_received = get_variation_for_feature_rollout(project_config, feature_flag, user_context)
178
+ decide_reasons.push(*reasons_received)
179
+ decisions << [rollout_decision, decide_reasons]
180
+ end
181
+ end
182
+ user_profile_tracker&.save_user_profile
183
+ decisions
158
184
  end
159
185
 
160
- def get_variation_for_feature_experiment(project_config, feature_flag, user_context, decide_options = [])
186
+ def get_variation_for_feature_experiment(project_config, feature_flag, user_context, user_profile_tracker, decide_options = [])
161
187
  # Gets the variation the user is bucketed into for the feature flag's experiment.
162
188
  #
163
189
  # project_config - project_config - Instance of ProjectConfig
@@ -187,7 +213,7 @@ module Optimizely
187
213
  end
188
214
 
189
215
  experiment_id = experiment['id']
190
- variation_id, reasons_received = get_variation_from_experiment_rule(project_config, feature_flag_key, experiment, user_context, decide_options)
216
+ variation_id, reasons_received = get_variation_from_experiment_rule(project_config, feature_flag_key, experiment, user_context, user_profile_tracker, decide_options)
191
217
  decide_reasons.push(*reasons_received)
192
218
 
193
219
  next unless variation_id
@@ -252,7 +278,7 @@ module Optimizely
252
278
  [nil, decide_reasons]
253
279
  end
254
280
 
255
- def get_variation_from_experiment_rule(project_config, flag_key, rule, user, options = [])
281
+ def get_variation_from_experiment_rule(project_config, flag_key, rule, user, user_profile_tracker, options = [])
256
282
  # Determine which variation the user is in for a given rollout.
257
283
  # Returns the variation from experiment rules.
258
284
  #
@@ -270,7 +296,7 @@ module Optimizely
270
296
 
271
297
  return [variation['id'], reasons] if variation
272
298
 
273
- variation_id, response_reasons = get_variation(project_config, rule['id'], user, options)
299
+ variation_id, response_reasons = get_variation(project_config, rule['id'], user, user_profile_tracker, options)
274
300
  reasons.push(*response_reasons)
275
301
 
276
302
  [variation_id, reasons]
@@ -122,11 +122,11 @@ module Optimizely
122
122
 
123
123
  return false unless variables.respond_to?(:each) && !variables.empty?
124
124
 
125
- is_valid = true
125
+ is_valid = true # rubocop:disable Lint/UselessAssignment
126
126
  if variables.include? :user_id
127
127
  # Empty str is a valid user ID.
128
128
  unless variables[:user_id].is_a?(String)
129
- is_valid = false
129
+ is_valid = false # rubocop:disable Lint/UselessAssignment
130
130
  logger.log(level, "#{Constants::INPUT_VARIABLES['USER_ID']} is invalid")
131
131
  end
132
132
  variables.delete :user_id
@@ -142,7 +142,6 @@ module Optimizely
142
142
  notification_center = nil,
143
143
  settings = nil
144
144
  )
145
-
146
145
  error_handler ||= NoOpErrorHandler.new
147
146
  logger ||= NoOpLogger.new
148
147
  notification_center = notification_center.is_a?(Optimizely::NotificationCenter) ? notification_center : NotificationCenter.new(logger, error_handler)
@@ -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.1.0'
21
21
  end
data/lib/optimizely.rb CHANGED
@@ -42,6 +42,7 @@ require_relative 'optimizely/optimizely_user_context'
42
42
  require_relative 'optimizely/odp/lru_cache'
43
43
  require_relative 'optimizely/odp/odp_manager'
44
44
  require_relative 'optimizely/helpers/sdk_settings'
45
+ require_relative 'optimizely/user_profile_tracker'
45
46
 
46
47
  module Optimizely
47
48
  class Project
@@ -172,65 +173,18 @@ module Optimizely
172
173
  OptimizelyUserContext.new(self, user_id, attributes)
173
174
  end
174
175
 
175
- def decide(user_context, key, decide_options = [])
176
- # raising on user context as it is internal and not provided directly by the user.
177
- raise if user_context.class != OptimizelyUserContext
178
-
179
- reasons = []
180
-
181
- # check if SDK is ready
182
- unless is_valid
183
- @logger.log(Logger::ERROR, InvalidProjectConfigError.new('decide').message)
184
- reasons.push(OptimizelyDecisionMessage::SDK_NOT_READY)
185
- return OptimizelyDecision.new(flag_key: key, user_context: user_context, reasons: reasons)
186
- end
187
-
188
- # validate that key is a string
189
- unless key.is_a?(String)
190
- @logger.log(Logger::ERROR, 'Provided key is invalid')
191
- reasons.push(format(OptimizelyDecisionMessage::FLAG_KEY_INVALID, key))
192
- return OptimizelyDecision.new(flag_key: key, user_context: user_context, reasons: reasons)
193
- end
194
-
195
- # validate that key maps to a feature flag
196
- config = project_config
197
- feature_flag = config.get_feature_flag_from_key(key)
198
- unless feature_flag
199
- @logger.log(Logger::ERROR, "No feature flag was found for key '#{key}'.")
200
- reasons.push(format(OptimizelyDecisionMessage::FLAG_KEY_INVALID, key))
201
- return OptimizelyDecision.new(flag_key: key, user_context: user_context, reasons: reasons)
202
- end
203
-
204
- # merge decide_options and default_decide_options
205
- if decide_options.is_a? Array
206
- decide_options += @default_decide_options
207
- else
208
- @logger.log(Logger::DEBUG, 'Provided decide options is not an array. Using default decide options.')
209
- decide_options = @default_decide_options
210
- end
211
-
176
+ def create_optimizely_decision(user_context, flag_key, decision, reasons, decide_options, config)
212
177
  # Create Optimizely Decision Result.
213
178
  user_id = user_context.user_id
214
179
  attributes = user_context.user_attributes
215
180
  variation_key = nil
216
181
  feature_enabled = false
217
182
  rule_key = nil
218
- flag_key = key
219
183
  all_variables = {}
220
184
  decision_event_dispatched = false
185
+ feature_flag = config.get_feature_flag_from_key(flag_key)
221
186
  experiment = nil
222
187
  decision_source = Optimizely::DecisionService::DECISION_SOURCES['ROLLOUT']
223
- context = Optimizely::OptimizelyUserContext::OptimizelyDecisionContext.new(key, nil)
224
- variation, reasons_received = @decision_service.validated_forced_decision(config, context, user_context)
225
- reasons.push(*reasons_received)
226
-
227
- if variation
228
- decision = Optimizely::DecisionService::Decision.new(nil, variation, Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST'])
229
- else
230
- decision, reasons_received = @decision_service.get_variation_for_feature(config, feature_flag, user_context, decide_options)
231
- reasons.push(*reasons_received)
232
- end
233
-
234
188
  # Send impression event if Decision came from a feature test and decide options doesn't include disableDecisionEvent
235
189
  if decision.is_a?(Optimizely::DecisionService::Decision)
236
190
  experiment = decision.experiment
@@ -249,7 +203,7 @@ module Optimizely
249
203
  # Generate all variables map if decide options doesn't include excludeVariables
250
204
  unless decide_options.include? OptimizelyDecideOption::EXCLUDE_VARIABLES
251
205
  feature_flag['variables'].each do |variable|
252
- variable_value = get_feature_variable_for_variation(key, feature_enabled, variation, variable, user_id)
206
+ variable_value = get_feature_variable_for_variation(flag_key, feature_enabled, variation, variable, user_id)
253
207
  all_variables[variable['key']] = Helpers::VariableType.cast_value_to_type(variable_value, variable['type'], @logger)
254
208
  end
255
209
  end
@@ -281,6 +235,47 @@ module Optimizely
281
235
  )
282
236
  end
283
237
 
238
+ def decide(user_context, key, decide_options = [])
239
+ # raising on user context as it is internal and not provided directly by the user.
240
+ raise if user_context.class != OptimizelyUserContext
241
+
242
+ reasons = []
243
+
244
+ # check if SDK is ready
245
+ unless is_valid
246
+ @logger.log(Logger::ERROR, InvalidProjectConfigError.new('decide').message)
247
+ reasons.push(OptimizelyDecisionMessage::SDK_NOT_READY)
248
+ return OptimizelyDecision.new(flag_key: key, user_context: user_context, reasons: reasons)
249
+ end
250
+
251
+ # validate that key is a string
252
+ unless key.is_a?(String)
253
+ @logger.log(Logger::ERROR, 'Provided key is invalid')
254
+ reasons.push(format(OptimizelyDecisionMessage::FLAG_KEY_INVALID, key))
255
+ return OptimizelyDecision.new(flag_key: key, user_context: user_context, reasons: reasons)
256
+ end
257
+
258
+ # validate that key maps to a feature flag
259
+ config = project_config
260
+ feature_flag = config.get_feature_flag_from_key(key)
261
+ unless feature_flag
262
+ @logger.log(Logger::ERROR, "No feature flag was found for key '#{key}'.")
263
+ reasons.push(format(OptimizelyDecisionMessage::FLAG_KEY_INVALID, key))
264
+ return OptimizelyDecision.new(flag_key: key, user_context: user_context, reasons: reasons)
265
+ end
266
+
267
+ # merge decide_options and default_decide_options
268
+ if decide_options.is_a? Array
269
+ decide_options += @default_decide_options
270
+ else
271
+ @logger.log(Logger::DEBUG, 'Provided decide options is not an array. Using default decide options.')
272
+ decide_options = @default_decide_options
273
+ end
274
+
275
+ decide_options.delete(OptimizelyDecideOption::ENABLED_FLAGS_ONLY) if decide_options.include?(OptimizelyDecideOption::ENABLED_FLAGS_ONLY)
276
+ decide_for_keys(user_context, [key], decide_options, true)[key]
277
+ end
278
+
284
279
  def decide_all(user_context, decide_options = [])
285
280
  # raising on user context as it is internal and not provided directly by the user.
286
281
  raise if user_context.class != OptimizelyUserContext
@@ -298,7 +293,7 @@ module Optimizely
298
293
  decide_for_keys(user_context, keys, decide_options)
299
294
  end
300
295
 
301
- def decide_for_keys(user_context, keys, decide_options = [])
296
+ def decide_for_keys(user_context, keys, decide_options = [], ignore_default_options = false) # rubocop:disable Style/OptionalBooleanParameter
302
297
  # raising on user context as it is internal and not provided directly by the user.
303
298
  raise if user_context.class != OptimizelyUserContext
304
299
 
@@ -308,13 +303,79 @@ module Optimizely
308
303
  return {}
309
304
  end
310
305
 
311
- enabled_flags_only = (!decide_options.nil? && (decide_options.include? OptimizelyDecideOption::ENABLED_FLAGS_ONLY)) || (@default_decide_options.include? OptimizelyDecideOption::ENABLED_FLAGS_ONLY)
306
+ # merge decide_options and default_decide_options
307
+ unless ignore_default_options
308
+ if decide_options.is_a?(Array)
309
+ decide_options += @default_decide_options
310
+ else
311
+ @logger.log(Logger::DEBUG, 'Provided decide options is not an array. Using default decide options.')
312
+ decide_options = @default_decide_options
313
+ end
314
+ end
315
+
316
+ # enabled_flags_only = (!decide_options.nil? && (decide_options.include? OptimizelyDecideOption::ENABLED_FLAGS_ONLY)) || (@default_decide_options.include? OptimizelyDecideOption::ENABLED_FLAGS_ONLY)
312
317
 
313
318
  decisions = {}
319
+ valid_keys = []
320
+ decision_reasons_dict = {}
321
+ config = project_config
322
+ return decisions unless config
323
+
324
+ flags_without_forced_decision = []
325
+ flag_decisions = {}
326
+
314
327
  keys.each do |key|
315
- decision = decide(user_context, key, decide_options)
316
- decisions[key] = decision unless enabled_flags_only && !decision.enabled
328
+ # Retrieve the feature flag from the project's feature flag key map
329
+ feature_flag = config.feature_flag_key_map[key]
330
+
331
+ # If the feature flag is nil, create a default OptimizelyDecision and move to the next key
332
+ if feature_flag.nil?
333
+ decisions[key] = OptimizelyDecision.new(nil, false, nil, nil, key, user_context, [])
334
+ next
335
+ end
336
+ valid_keys.push(key)
337
+ decision_reasons = []
338
+ decision_reasons_dict[key] = decision_reasons
339
+
340
+ config = project_config
341
+ context = Optimizely::OptimizelyUserContext::OptimizelyDecisionContext.new(key, nil)
342
+ variation, reasons_received = @decision_service.validated_forced_decision(config, context, user_context)
343
+ decision_reasons_dict[key].push(*reasons_received)
344
+ if variation
345
+ decision = Optimizely::DecisionService::Decision.new(nil, variation, Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST'])
346
+ flag_decisions[key] = decision
347
+ else
348
+ flags_without_forced_decision.push(feature_flag)
349
+ end
317
350
  end
351
+ decision_list = @decision_service.get_variations_for_feature_list(config, flags_without_forced_decision, user_context, decide_options)
352
+
353
+ flags_without_forced_decision.each_with_index do |flag, i|
354
+ decision = decision_list[i][0]
355
+ reasons = decision_list[i][1]
356
+ flag_key = flag['key']
357
+ flag_decisions[flag_key] = decision
358
+ decision_reasons_dict[flag_key] ||= []
359
+ decision_reasons_dict[flag_key].push(*reasons)
360
+ end
361
+ valid_keys.each do |key|
362
+ flag_decision = flag_decisions[key]
363
+ decision_reasons = decision_reasons_dict[key]
364
+ optimizely_decision = create_optimizely_decision(
365
+ user_context,
366
+ key,
367
+ flag_decision,
368
+ decision_reasons,
369
+ decide_options,
370
+ config
371
+ )
372
+
373
+ enabled_flags_only_missing = !decide_options.include?(OptimizelyDecideOption::ENABLED_FLAGS_ONLY)
374
+ is_enabled = optimizely_decision.enabled
375
+
376
+ decisions[key] = optimizely_decision if enabled_flags_only_missing || is_enabled
377
+ end
378
+
318
379
  decisions
319
380
  end
320
381
 
@@ -959,7 +1020,10 @@ module Optimizely
959
1020
  return nil unless user_inputs_valid?(attributes)
960
1021
 
961
1022
  user_context = OptimizelyUserContext.new(self, user_id, attributes, identify: false)
962
- variation_id, = @decision_service.get_variation(config, experiment_id, user_context)
1023
+ user_profile_tracker = UserProfileTracker.new(user_id, @user_profile_service, @logger)
1024
+ user_profile_tracker.load_user_profile
1025
+ variation_id, = @decision_service.get_variation(config, experiment_id, user_context, user_profile_tracker)
1026
+ user_profile_tracker.save_user_profile
963
1027
  variation = config.get_variation_from_id(experiment_key, variation_id) unless variation_id.nil?
964
1028
  variation_key = variation['key'] if variation
965
1029
  decision_notification_type = if config.feature_experiment?(experiment_id)
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.0.1
4
+ version: 5.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Optimizely
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-02-08 00:00:00.000000000 Z
11
+ date: 2025-01-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -191,6 +191,7 @@ files:
191
191
  - lib/optimizely/semantic_version.rb
192
192
  - lib/optimizely/user_condition_evaluator.rb
193
193
  - lib/optimizely/user_profile_service.rb
194
+ - lib/optimizely/user_profile_tracker.rb
194
195
  - lib/optimizely/version.rb
195
196
  homepage: https://github.com/optimizely/ruby-sdk
196
197
  licenses:
@@ -213,7 +214,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
213
214
  - !ruby/object:Gem::Version
214
215
  version: '0'
215
216
  requirements: []
216
- rubygems_version: 3.4.10
217
+ rubygems_version: 3.4.19
217
218
  signing_key:
218
219
  specification_version: 4
219
220
  summary: Ruby SDK for Optimizely's testing framework