optimizely-sdk 5.0.1 → 5.1.0

Sign up to get free protection for your applications and to get access to all the features.
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