• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

optimizely / ruby-sdk / 2016

pending completion
2016

cron

travis-ci-com

web-flow
refact(forced-decision): Support for decision context added. (#288)

* added struct implementation for forced decision

* doc string updated

* variation updated to variation_key

* duplicate OptimizelyDecisionContext testcase added

* flag key empty allowed

167 of 167 new or added lines in 4 files covered. (100.0%)

7869 of 7900 relevant lines covered (99.61%)

6321.92 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.8
/lib/optimizely/decision_service.rb
1
# frozen_string_literal: true
2

3
#
4
#    Copyright 2017-2021, Optimizely and contributors
5
#
6
#    Licensed under the Apache License, Version 2.0 (the "License");
7
#    you may not use this file except in compliance with the License.
8
#    You may obtain a copy of the License at
9
#
10
#        http://www.apache.org/licenses/LICENSE-2.0
11
#
12
#    Unless required by applicable law or agreed to in writing, software
13
#    distributed under the License is distributed on an "AS IS" BASIS,
14
#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
#    See the License for the specific language governing permissions and
16
#    limitations under the License.
17
#
18
require_relative './bucketer'
4✔
19

20
module Optimizely
4✔
21
  class DecisionService
4✔
22
    # Optimizely's decision service that determines into which variation of an experiment a user will be allocated.
23
    #
24
    # The decision service contains all logic relating to how a user bucketing decisions is made.
25
    # This includes all of the following (in order):
26
    #
27
    # 1. Check experiment status
28
    # 2. Check forced bucketing
29
    # 3. Check whitelisting
30
    # 4. Check user profile service for past bucketing decisions (sticky bucketing)
31
    # 5. Check audience targeting
32
    # 6. Use Murmurhash3 to bucket the user
33

34
    attr_reader :bucketer
4✔
35

36
    # Hash of user IDs to a Hash of experiments to variations.
37
    # This contains all the forced variations set by the user by calling setForcedVariation.
38
    attr_reader :forced_variation_map
4✔
39

40
    Decision = Struct.new(:experiment, :variation, :source)
4✔
41

42
    DECISION_SOURCES = {
3✔
43
      'EXPERIMENT' => 'experiment',
1✔
44
      'FEATURE_TEST' => 'feature-test',
45
      'ROLLOUT' => 'rollout'
46
    }.freeze
47

48
    def initialize(logger, user_profile_service = nil)
4✔
49
      @logger = logger
1,372✔
50
      @user_profile_service = user_profile_service
1,372✔
51
      @bucketer = Bucketer.new(logger)
1,372✔
52
      @forced_variation_map = {}
1,372✔
53
    end
54

55
    def get_variation(project_config, experiment_id, user_context, decide_options = [])
4✔
56
      # Determines variation into which user will be bucketed.
57
      #
58
      # project_config - project_config - Instance of ProjectConfig
59
      # experiment_id - Experiment for which visitor variation needs to be determined
60
      # user_context - Optimizely user context instance
61
      #
62
      # Returns variation ID where visitor will be bucketed
63
      #   (nil if experiment is inactive or user does not meet audience conditions)
64

65
      decide_reasons = []
376✔
66
      user_id = user_context.user_id
376✔
67
      attributes = user_context.user_attributes
376✔
68
      # By default, the bucketing ID should be the user ID
69
      bucketing_id, bucketing_id_reasons = get_bucketing_id(user_id, attributes)
376✔
70
      decide_reasons.push(*bucketing_id_reasons)
376✔
71
      # Check to make sure experiment is active
72
      experiment = project_config.get_experiment_from_id(experiment_id)
376✔
73
      return nil, decide_reasons if experiment.nil?
376✔
74

75
      experiment_key = experiment['key']
372✔
76
      unless project_config.experiment_running?(experiment)
372✔
77
        message = "Experiment '#{experiment_key}' is not running."
20✔
78
        @logger.log(Logger::INFO, message)
20✔
79
        decide_reasons.push(message)
20✔
80
        return nil, decide_reasons
20✔
81
      end
82

83
      # Check if a forced variation is set for the user
84
      forced_variation, reasons_received = get_forced_variation(project_config, experiment['key'], user_id)
352✔
85
      decide_reasons.push(*reasons_received)
352✔
86
      return forced_variation['id'], decide_reasons if forced_variation
352✔
87

88
      # Check if user is in a white-listed variation
89
      whitelisted_variation_id, reasons_received = get_whitelisted_variation_id(project_config, experiment_id, user_id)
324✔
90
      decide_reasons.push(*reasons_received)
324✔
91
      return whitelisted_variation_id, decide_reasons if whitelisted_variation_id
324✔
92

93
      should_ignore_user_profile_service = decide_options.include? Optimizely::Decide::OptimizelyDecideOption::IGNORE_USER_PROFILE_SERVICE
292✔
94
      # Check for saved bucketing decisions if decide_options do not include ignoreUserProfileService
95
      unless should_ignore_user_profile_service
292✔
96
        user_profile, reasons_received = get_user_profile(user_id)
288✔
97
        decide_reasons.push(*reasons_received)
288✔
98
        saved_variation_id, reasons_received = get_saved_variation_id(project_config, experiment_id, user_profile)
288✔
99
        decide_reasons.push(*reasons_received)
288✔
100
        if saved_variation_id
288✔
101
          message = "Returning previously activated variation ID #{saved_variation_id} of experiment '#{experiment_key}' for user '#{user_id}' from user profile."
4✔
102
          @logger.log(Logger::INFO, message)
4✔
103
          decide_reasons.push(message)
4✔
104
          return saved_variation_id, decide_reasons
4✔
105
        end
106
      end
107

108
      # Check audience conditions
109
      user_meets_audience_conditions, reasons_received = Audience.user_meets_audience_conditions?(project_config, experiment, attributes, @logger)
288✔
110
      decide_reasons.push(*reasons_received)
288✔
111
      unless user_meets_audience_conditions
288✔
112
        message = "User '#{user_id}' does not meet the conditions to be in experiment '#{experiment_key}'."
100✔
113
        @logger.log(Logger::INFO, message)
100✔
114
        decide_reasons.push(message)
100✔
115
        return nil, decide_reasons
100✔
116
      end
117

118
      # Bucket normally
119
      variation, bucket_reasons = @bucketer.bucket(project_config, experiment, bucketing_id, user_id)
188✔
120
      decide_reasons.push(*bucket_reasons)
188✔
121
      variation_id = variation ? variation['id'] : nil
188✔
122

123
      message = ''
188✔
124
      if variation_id
188✔
125
        variation_key = variation['key']
180✔
126
        message = "User '#{user_id}' is in variation '#{variation_key}' of experiment '#{experiment_id}'."
180✔
127
      else
128
        message = "User '#{user_id}' is in no variation."
8✔
129
      end
130
      @logger.log(Logger::INFO, message)
188✔
131
      decide_reasons.push(message)
188✔
132

133
      # Persist bucketing decision
134
      save_user_profile(user_profile, experiment_id, variation_id) unless should_ignore_user_profile_service
188✔
135
      [variation_id, decide_reasons]
188✔
136
    end
137

138
    def get_variation_for_feature(project_config, feature_flag, user_context, decide_options = [])
4✔
139
      # Get the variation the user is bucketed into for the given FeatureFlag.
140
      #
141
      # project_config - project_config - Instance of ProjectConfig
142
      # feature_flag - The feature flag the user wants to access
143
      # user_context - Optimizely user context instance
144
      #
145
      # Returns Decision struct (nil if the user is not bucketed into any of the experiments on the feature)
146

147
      decide_reasons = []
220✔
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)
220✔
151
      decide_reasons.push(*reasons_received)
220✔
152
      return decision, decide_reasons unless decision.nil?
220✔
153

154
      decision, reasons_received = get_variation_for_feature_rollout(project_config, feature_flag, user_context)
156✔
155
      decide_reasons.push(*reasons_received)
156✔
156

157
      [decision, decide_reasons]
156✔
158
    end
159

160
    def get_variation_for_feature_experiment(project_config, feature_flag, user_context, decide_options = [])
4✔
161
      # Gets the variation the user is bucketed into for the feature flag's experiment.
162
      #
163
      # project_config - project_config - Instance of ProjectConfig
164
      # feature_flag - The feature flag the user wants to access
165
      # user_context - Optimizely user context instance
166
      #
167
      # Returns Decision struct (nil if the user is not bucketed into any of the experiments on the feature)
168
      # or nil if the user is not bucketed into any of the experiments on the feature
169
      decide_reasons = []
232✔
170
      user_id = user_context.user_id
232✔
171
      feature_flag_key = feature_flag['key']
232✔
172
      if feature_flag['experimentIds'].empty?
232✔
173
        message = "The feature flag '#{feature_flag_key}' is not used in any experiments."
80✔
174
        @logger.log(Logger::DEBUG, message)
80✔
175
        decide_reasons.push(message)
80✔
176
        return nil, decide_reasons
80✔
177
      end
178

179
      # Evaluate each experiment and return the first bucketed experiment variation
180
      feature_flag['experimentIds'].each do |experiment_id|
152✔
181
        experiment = project_config.experiment_id_map[experiment_id]
156✔
182
        unless experiment
156✔
183
          message = "Feature flag experiment with ID '#{experiment_id}' is not in the datafile."
4✔
184
          @logger.log(Logger::DEBUG, message)
4✔
185
          decide_reasons.push(message)
4✔
186
          return nil, decide_reasons
4✔
187
        end
188

189
        experiment_id = experiment['id']
152✔
190
        variation_id, reasons_received = get_variation_from_experiment_rule(project_config, feature_flag_key, experiment, user_context, decide_options)
152✔
191
        decide_reasons.push(*reasons_received)
152✔
192

193
        next unless variation_id
152✔
194

195
        variation = project_config.get_variation_from_id_by_experiment_id(experiment_id, variation_id)
68✔
196

197
        return Decision.new(experiment, variation, DECISION_SOURCES['FEATURE_TEST']), decide_reasons
68✔
198
      end
199

200
      message = "The user '#{user_id}' is not bucketed into any of the experiments on the feature '#{feature_flag_key}'."
80✔
201
      @logger.log(Logger::INFO, message)
80✔
202
      decide_reasons.push(message)
80✔
203

204
      [nil, decide_reasons]
80✔
205
    end
206

207
    def get_variation_for_feature_rollout(project_config, feature_flag, user_context)
4✔
208
      # Determine which variation the user is in for a given rollout.
209
      # Returns the variation of the first experiment the user qualifies for.
210
      #
211
      # project_config - project_config - Instance of ProjectConfig
212
      # feature_flag - The feature flag the user wants to access
213
      # user_context - Optimizely user context instance
214
      #
215
      # Returns the Decision struct or nil if not bucketed into any of the targeting rules
216
      decide_reasons = []
180✔
217

218
      rollout_id = feature_flag['rolloutId']
180✔
219
      feature_flag_key = feature_flag['key']
180✔
220
      if rollout_id.nil? || rollout_id.empty?
180✔
221
        message = "Feature flag '#{feature_flag_key}' is not used in a rollout."
68✔
222
        @logger.log(Logger::DEBUG, message)
68✔
223
        decide_reasons.push(message)
68✔
224
        return nil, decide_reasons
68✔
225
      end
226

227
      rollout = project_config.get_rollout_from_id(rollout_id)
112✔
228
      if rollout.nil?
112✔
229
        message = "Rollout with ID '#{rollout_id}' is not in the datafile '#{feature_flag['key']}'"
12✔
230
        @logger.log(Logger::DEBUG, message)
12✔
231
        decide_reasons.push(message)
12✔
232
        return nil, decide_reasons
12✔
233
      end
234

235
      return nil, decide_reasons if rollout['experiments'].empty?
100✔
236

237
      index = 0
96✔
238
      rollout_rules = rollout['experiments']
96✔
239
      while index < rollout_rules.length
96✔
240
        variation, skip_to_everyone_else, reasons_received = get_variation_from_delivery_rule(project_config, feature_flag_key, rollout_rules, index, user_context)
200✔
241
        decide_reasons.push(*reasons_received)
200✔
242
        if variation
200✔
243
          rule = rollout_rules[index]
72✔
244
          feature_decision = Decision.new(rule, variation, DECISION_SOURCES['ROLLOUT'])
72✔
245
          return [feature_decision, decide_reasons]
72✔
246
        end
247

248
        index = skip_to_everyone_else ? (rollout_rules.length - 1) : (index + 1)
128✔
249
      end
250

251
      [nil, decide_reasons]
24✔
252
    end
253

254
    def get_variation_from_experiment_rule(project_config, flag_key, rule, user, options = [])
4✔
255
      # Determine which variation the user is in for a given rollout.
256
      # Returns the variation from experiment rules.
257
      #
258
      # project_config - project_config - Instance of ProjectConfig
259
      # flag_key - The feature flag the user wants to access
260
      # rule - An experiment rule key
261
      # user - Optimizely user context instance
262
      #
263
      # Returns variation_id and reasons
264
      reasons = []
152✔
265

266
      context = Optimizely::OptimizelyUserContext::OptimizelyDecisionContext.new(flag_key, rule['key'])
152✔
267
      variation, forced_reasons = user.find_validated_forced_decision(context)
152✔
268
      reasons.push(*forced_reasons)
152✔
269

270
      return [variation['id'], reasons] if variation
152✔
271

272
      variation_id, response_reasons = get_variation(project_config, rule['id'], user, options)
148✔
273
      reasons.push(*response_reasons)
148✔
274

275
      [variation_id, reasons]
148✔
276
    end
277

278
    def get_variation_from_delivery_rule(project_config, flag_key, rules, rule_index, user)
4✔
279
      # Determine which variation the user is in for a given rollout.
280
      # Returns the variation from delivery rules.
281
      #
282
      # project_config - project_config - Instance of ProjectConfig
283
      # flag_key - The feature flag the user wants to access
284
      # rule - An experiment rule key
285
      # user - Optimizely user context instance
286
      #
287
      # Returns variation, boolean to skip for eveyone else rule and reasons
288
      reasons = []
200✔
289
      skip_to_everyone_else = false
200✔
290
      rule = rules[rule_index]
200✔
291
      context = Optimizely::OptimizelyUserContext::OptimizelyDecisionContext.new(flag_key, rule['key'])
200✔
292
      variation, forced_reasons = user.find_validated_forced_decision(context)
200✔
293
      reasons.push(*forced_reasons)
200✔
294

295
      return [variation, skip_to_everyone_else, reasons] if variation
200✔
296

297
      user_id = user.user_id
192✔
298
      attributes = user.user_attributes
192✔
299
      bucketing_id, bucketing_id_reasons = get_bucketing_id(user_id, attributes)
192✔
300
      reasons.push(*bucketing_id_reasons)
192✔
301

302
      everyone_else = (rule_index == rules.length - 1)
192✔
303

304
      logging_key = everyone_else ? 'Everyone Else' : (rule_index + 1).to_s
192✔
305

306
      user_meets_audience_conditions, reasons_received = Audience.user_meets_audience_conditions?(project_config, rule, attributes, @logger, 'ROLLOUT_AUDIENCE_EVALUATION_LOGS', logging_key)
192✔
307
      reasons.push(*reasons_received)
192✔
308
      unless user_meets_audience_conditions
192✔
309
        message = "User '#{user_id}' does not meet the conditions for targeting rule '#{logging_key}'."
100✔
310
        @logger.log(Logger::DEBUG, message)
100✔
311
        reasons.push(message)
100✔
312
        return [nil, skip_to_everyone_else, reasons]
100✔
313
      end
314

315
      message = "User '#{user_id}' meets the audience conditions for targeting rule '#{logging_key}'."
92✔
316
      @logger.log(Logger::DEBUG, message)
92✔
317
      reasons.push(message)
92✔
318
      bucket_variation, bucket_reasons = @bucketer.bucket(project_config, rule, bucketing_id, user_id)
92✔
319

320
      reasons.push(*bucket_reasons)
92✔
321

322
      if bucket_variation
92✔
323
        message = "User '#{user_id}' is in the traffic group of targeting rule '#{logging_key}'."
64✔
324
        @logger.log(Logger::DEBUG, message)
64✔
325
        reasons.push(message)
64✔
326
      elsif !everyone_else
28✔
327
        message = "User '#{user_id}' is not in the traffic group for targeting rule '#{logging_key}'."
16✔
328
        @logger.log(Logger::DEBUG, message)
16✔
329
        reasons.push(message)
16✔
330
        skip_to_everyone_else = true
16✔
331
      end
332
      [bucket_variation, skip_to_everyone_else, reasons]
92✔
333
    end
334

335
    def set_forced_variation(project_config, experiment_key, user_id, variation_key)
4✔
336
      # Sets a Hash of user IDs to a Hash of experiments to forced variations.
337
      #
338
      # project_config - Instance of ProjectConfig
339
      # experiment_key - String Key for experiment
340
      # user_id - String ID for user.
341
      # variation_key - String Key for variation. If null, then clear the existing experiment-to-variation mapping
342
      #
343
      # Returns a boolean value that indicates if the set completed successfully
344

345
      experiment = project_config.get_experiment_from_key(experiment_key)
84✔
346
      experiment_id = experiment['id'] if experiment
84✔
347
      #  check if the experiment exists in the datafile
348
      return false if experiment_id.nil? || experiment_id.empty?
84✔
349

350
      #  clear the forced variation if the variation key is null
351
      if variation_key.nil?
80✔
352
        @forced_variation_map[user_id].delete(experiment_id) if @forced_variation_map.key? user_id
×
353
        @logger.log(Logger::DEBUG, "Variation mapped to experiment '#{experiment_key}' has been removed for user "\
×
354
                    "'#{user_id}'.")
355
        return true
×
356
      end
357

358
      variation_id = project_config.get_variation_id_from_key_by_experiment_id(experiment_id, variation_key)
80✔
359

360
      #  check if the variation exists in the datafile
361
      unless variation_id
80✔
362
        #  this case is logged in get_variation_id_from_key
363
        return false
4✔
364
      end
365

366
      @forced_variation_map[user_id] = {} unless @forced_variation_map.key? user_id
76✔
367
      @forced_variation_map[user_id][experiment_id] = variation_id
76✔
368
      @logger.log(Logger::DEBUG, "Set variation '#{variation_id}' for experiment '#{experiment_id}' and "\
76✔
369
                  "user '#{user_id}' in the forced variation map.")
370
      true
76✔
371
    end
372

373
    def get_forced_variation(project_config, experiment_key, user_id)
4✔
374
      # Gets the forced variation for the given user and experiment.
375
      #
376
      # project_config - Instance of ProjectConfig
377
      # experiment_key - String key for experiment
378
      # user_id - String ID for user
379
      #
380
      # Returns Variation The variation which the given user and experiment should be forced into
381

382
      decide_reasons = []
400✔
383
      unless @forced_variation_map.key? user_id
400✔
384
        message = "User '#{user_id}' is not in the forced variation map."
336✔
385
        @logger.log(Logger::DEBUG, message)
336✔
386
        return nil, decide_reasons
336✔
387
      end
388

389
      experiment_to_variation_map = @forced_variation_map[user_id]
64✔
390
      experiment = project_config.get_experiment_from_key(experiment_key)
64✔
391
      experiment_id = experiment['id'] if experiment
64✔
392
      # check for nil and empty string experiment ID
393
      # this case is logged in get_experiment_from_key
394
      return nil, decide_reasons if experiment_id.nil? || experiment_id.empty?
64✔
395

396
      unless experiment_to_variation_map.key? experiment_id
64✔
397
        message = "No experiment '#{experiment_id}' mapped to user '#{user_id}' in the forced variation map."
4✔
398
        @logger.log(Logger::DEBUG, message)
4✔
399
        decide_reasons.push(message)
4✔
400
        return nil, decide_reasons
4✔
401
      end
402

403
      variation_id = experiment_to_variation_map[experiment_id]
60✔
404
      variation_key = ''
60✔
405
      variation = project_config.get_variation_from_id_by_experiment_id(experiment_id, variation_id)
60✔
406
      variation_key = variation['key'] if variation
60✔
407

408
      # check if the variation exists in the datafile
409
      # this case is logged in get_variation_from_id
410
      return nil, decide_reasons if variation_key.empty?
60✔
411

412
      message = "Variation '#{variation_key}' is mapped to experiment '#{experiment_id}' and user '#{user_id}' in the forced variation map"
60✔
413
      @logger.log(Logger::DEBUG, message)
60✔
414
      decide_reasons.push(message)
60✔
415

416
      [variation, decide_reasons]
60✔
417
    end
418

419
    private
4✔
420

421
    def get_whitelisted_variation_id(project_config, experiment_id, user_id)
4✔
422
      # Determine if a user is whitelisted into a variation for the given experiment and return the ID of that variation
423
      #
424
      # project_config - project_config - Instance of ProjectConfig
425
      # experiment_key - Key representing the experiment for which user is to be bucketed
426
      # user_id - ID for the user
427
      #
428
      # Returns variation ID into which user_id is whitelisted (nil if no variation)
429

430
      whitelisted_variations = project_config.get_whitelisted_variations(experiment_id)
324✔
431

432
      return nil, nil unless whitelisted_variations
324✔
433

434
      whitelisted_variation_key = whitelisted_variations[user_id]
324✔
435

436
      return nil, nil unless whitelisted_variation_key
324✔
437

438
      whitelisted_variation_id = project_config.get_variation_id_from_key_by_experiment_id(experiment_id, whitelisted_variation_key)
36✔
439

440
      unless whitelisted_variation_id
36✔
441
        message = "User '#{user_id}' is whitelisted into variation '#{whitelisted_variation_key}', which is not in the datafile."
4✔
442
        @logger.log(Logger::INFO, message)
4✔
443
        return nil, message
4✔
444
      end
445

446
      message = "User '#{user_id}' is whitelisted into variation '#{whitelisted_variation_key}' of experiment '#{experiment_id}'."
32✔
447
      @logger.log(Logger::INFO, message)
32✔
448

449
      [whitelisted_variation_id, message]
32✔
450
    end
451

452
    def get_saved_variation_id(project_config, experiment_id, user_profile)
4✔
453
      # Retrieve variation ID of stored bucketing decision for a given experiment from a given user profile
454
      #
455
      # project_config - project_config - Instance of ProjectConfig
456
      # experiment_id - String experiment ID
457
      # user_profile - Hash user profile
458
      #
459
      # Returns string variation ID (nil if no decision is found)
460
      return nil, nil unless user_profile[:experiment_bucket_map]
288✔
461

462
      decision = user_profile[:experiment_bucket_map][experiment_id]
288✔
463
      return nil, nil unless decision
288✔
464

465
      variation_id = decision[:variation_id]
8✔
466
      return variation_id, nil if project_config.variation_id_exists?(experiment_id, variation_id)
8✔
467

468
      message = "User '#{user_profile[:user_id]}' was previously bucketed into variation ID '#{variation_id}' for experiment '#{experiment_id}', but no matching variation was found. Re-bucketing user."
4✔
469
      @logger.log(Logger::INFO, message)
4✔
470

471
      [nil, message]
4✔
472
    end
473

474
    def get_user_profile(user_id)
4✔
475
      # Determine if a user is forced into a variation for the given experiment and return the ID of that variation
476
      #
477
      # user_id - String ID for the user
478
      #
479
      # Returns Hash stored user profile (or a default one if lookup fails or user profile service not provided)
480

481
      user_profile = {
216✔
482
        user_id: user_id,
72✔
483
        experiment_bucket_map: {}
484
      }
485

486
      return user_profile, nil unless @user_profile_service
288✔
487

488
      message = nil
48✔
489
      begin
36✔
490
        user_profile = @user_profile_service.lookup(user_id) || user_profile
48✔
491
      rescue => e
492
        message = "Error while looking up user profile for user ID '#{user_id}': #{e}."
4✔
493
        @logger.log(Logger::ERROR, message)
4✔
494
      end
495

496
      [user_profile, message]
48✔
497
    end
498

499
    def save_user_profile(user_profile, experiment_id, variation_id)
4✔
500
      # Save a given bucketing decision to a given user profile
501
      #
502
      # user_profile - Hash user profile
503
      # experiment_id - String experiment ID
504
      # variation_id - String variation ID
505

506
      return unless @user_profile_service
184✔
507

508
      user_id = user_profile[:user_id]
40✔
509
      begin
30✔
510
        user_profile[:experiment_bucket_map][experiment_id] = {
40✔
511
          variation_id: variation_id
512
        }
513
        @user_profile_service.save(user_profile)
40✔
514
        @logger.log(Logger::INFO, "Saved variation ID #{variation_id} of experiment ID #{experiment_id} for user '#{user_id}'.")
36✔
515
      rescue => e
3✔
516
        @logger.log(Logger::ERROR, "Error while saving user profile for user ID '#{user_id}': #{e}.")
4✔
517
      end
518
    end
519

520
    def get_bucketing_id(user_id, attributes)
4✔
521
      # Gets the Bucketing Id for Bucketing
522
      #
523
      # user_id - String user ID
524
      # attributes - Hash user attributes
525
      # Returns String representing bucketing ID if it is a String type in attributes else return user ID
526

527
      return user_id, nil unless attributes
584✔
528

529
      bucketing_id = attributes[Optimizely::Helpers::Constants::CONTROL_ATTRIBUTES['BUCKETING_ID']]
584✔
530

531
      if bucketing_id
584✔
532
        return bucketing_id, nil if bucketing_id.is_a?(String)
40✔
533

534
        message = 'Bucketing ID attribute is not a string. Defaulted to user ID.'
4✔
535
        @logger.log(Logger::WARN, message)
4✔
536
      end
537
      [user_id, message]
548✔
538
    end
539
  end
540
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc