optimizely-sdk 1.4.0 → 1.5.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.rb +223 -11
- data/lib/optimizely/bucketer.rb +19 -17
- data/lib/optimizely/decision_service.rb +245 -50
- data/lib/optimizely/event_builder.rb +33 -18
- data/lib/optimizely/exceptions.rb +9 -0
- data/lib/optimizely/helpers/constants.rb +7 -0
- data/lib/optimizely/helpers/variable_type.rb +56 -0
- data/lib/optimizely/notification_center.rb +148 -0
- data/lib/optimizely/project_config.rb +91 -37
- data/lib/optimizely/version.rb +2 -2
- metadata +5 -3
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA1:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 3fae777d6ce539eb2e0809dbf38d4784130c8ade
         | 
| 4 | 
            +
              data.tar.gz: f65bee6796830490f4ee8f1a86da20ed7b4f01fb
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: b8e6f2732d692b1844d3f0c990010aba9858c93b82244803962810d33c78b945f925936874097272705a1a7e724f9d3abad1c4c66ba358573ba32c8bd10d8515
         | 
| 7 | 
            +
              data.tar.gz: a8804feff56093f399f87425869a733e9b91b74f502b075a1d790364f47da5e524c36ecde23a626d2b8c977c344e5bd2fb5c93e742ceb5a2cfcf4a50710b9444
         | 
    
        data/lib/optimizely.rb
    CHANGED
    
    | @@ -19,9 +19,12 @@ require_relative 'optimizely/error_handler' | |
| 19 19 | 
             
            require_relative 'optimizely/event_builder'
         | 
| 20 20 | 
             
            require_relative 'optimizely/event_dispatcher'
         | 
| 21 21 | 
             
            require_relative 'optimizely/exceptions'
         | 
| 22 | 
            +
            require_relative 'optimizely/helpers/constants'
         | 
| 22 23 | 
             
            require_relative 'optimizely/helpers/group'
         | 
| 23 24 | 
             
            require_relative 'optimizely/helpers/validator'
         | 
| 25 | 
            +
            require_relative 'optimizely/helpers/variable_type'
         | 
| 24 26 | 
             
            require_relative 'optimizely/logger'
         | 
| 27 | 
            +
            require_relative 'optimizely/notification_center'
         | 
| 25 28 | 
             
            require_relative 'optimizely/project_config'
         | 
| 26 29 |  | 
| 27 30 | 
             
            module Optimizely
         | 
| @@ -36,6 +39,7 @@ module Optimizely | |
| 36 39 | 
             
                attr_reader :event_builder
         | 
| 37 40 | 
             
                attr_reader :event_dispatcher
         | 
| 38 41 | 
             
                attr_reader :logger
         | 
| 42 | 
            +
                attr_reader :notification_center
         | 
| 39 43 |  | 
| 40 44 | 
             
                def initialize(datafile, event_dispatcher = nil, logger = nil, error_handler = nil, skip_json_validation = false, user_profile_service = nil)
         | 
| 41 45 | 
             
                  # Constructor for Projects.
         | 
| @@ -81,6 +85,7 @@ module Optimizely | |
| 81 85 |  | 
| 82 86 | 
             
                  @decision_service = DecisionService.new(@config, @user_profile_service)
         | 
| 83 87 | 
             
                  @event_builder = EventBuilder.new(@config)
         | 
| 88 | 
            +
                  @notification_center = NotificationCenter.new(@logger, @error_handler)
         | 
| 84 89 | 
             
                end
         | 
| 85 90 |  | 
| 86 91 | 
             
                def activate(experiment_key, user_id, attributes = nil)
         | 
| @@ -107,17 +112,8 @@ module Optimizely | |
| 107 112 | 
             
                  end
         | 
| 108 113 |  | 
| 109 114 | 
             
                  # Create and dispatch impression event
         | 
| 110 | 
            -
                  variation_id = @config.get_variation_id_from_key(experiment_key, variation_key)
         | 
| 111 115 | 
             
                  experiment = @config.get_experiment_from_key(experiment_key)
         | 
| 112 | 
            -
                   | 
| 113 | 
            -
                  @logger.log(Logger::INFO,
         | 
| 114 | 
            -
                              'Dispatching impression event to URL %s with params %s.' % [impression_event.url,
         | 
| 115 | 
            -
                                                                                          impression_event.params])
         | 
| 116 | 
            -
                  begin
         | 
| 117 | 
            -
                    @event_dispatcher.dispatch_event(impression_event)
         | 
| 118 | 
            -
                  rescue => e
         | 
| 119 | 
            -
                    @logger.log(Logger::ERROR, "Unable to dispatch impression event. Error: #{e}")
         | 
| 120 | 
            -
                  end
         | 
| 116 | 
            +
                  send_impression(experiment, variation_key, user_id, attributes)
         | 
| 121 117 |  | 
| 122 118 | 
             
                  variation_key
         | 
| 123 119 | 
             
                end
         | 
| @@ -151,7 +147,10 @@ module Optimizely | |
| 151 147 | 
             
                  variation_id = @decision_service.get_variation(experiment_key, user_id, attributes)
         | 
| 152 148 |  | 
| 153 149 | 
             
                  unless variation_id.nil?
         | 
| 154 | 
            -
                     | 
| 150 | 
            +
                    variation = @config.get_variation_from_id(experiment_key, variation_id)
         | 
| 151 | 
            +
                    if variation
         | 
| 152 | 
            +
                      return variation['key']
         | 
| 153 | 
            +
                    end
         | 
| 155 154 | 
             
                  end
         | 
| 156 155 | 
             
                  nil
         | 
| 157 156 | 
             
                end
         | 
| @@ -235,10 +234,204 @@ module Optimizely | |
| 235 234 | 
             
                  rescue => e
         | 
| 236 235 | 
             
                    @logger.log(Logger::ERROR, "Unable to dispatch conversion event. Error: #{e}")
         | 
| 237 236 | 
             
                  end
         | 
| 237 | 
            +
                  @notification_center.send_notifications(
         | 
| 238 | 
            +
                      NotificationCenter::NOTIFICATION_TYPES[:TRACK],
         | 
| 239 | 
            +
                      event_key, user_id, attributes, event_tags, conversion_event
         | 
| 240 | 
            +
                  )
         | 
| 241 | 
            +
                end
         | 
| 242 | 
            +
             | 
| 243 | 
            +
                def is_feature_enabled(feature_flag_key, user_id, attributes = nil)
         | 
| 244 | 
            +
                  # Determine whether a feature is enabled.
         | 
| 245 | 
            +
                  # Sends an impression event if the user is bucketed into an experiment using the feature.
         | 
| 246 | 
            +
                  #
         | 
| 247 | 
            +
                  # feature_flag_key - String unique key of the feature.
         | 
| 248 | 
            +
                  # userId - String ID of the user.
         | 
| 249 | 
            +
                  # attributes - Hash representing visitor attributes and values which need to be recorded.
         | 
| 250 | 
            +
                  #
         | 
| 251 | 
            +
                  # Returns True if the feature is enabled.
         | 
| 252 | 
            +
                  #         False if the feature is disabled.
         | 
| 253 | 
            +
                  #         False if the feature is not found.
         | 
| 254 | 
            +
             | 
| 255 | 
            +
                  unless @is_valid
         | 
| 256 | 
            +
                    logger = SimpleLogger.new
         | 
| 257 | 
            +
                    logger.log(Logger::ERROR, InvalidDatafileError.new('is_feature_enabled').message)
         | 
| 258 | 
            +
                    return nil
         | 
| 259 | 
            +
                  end
         | 
| 260 | 
            +
             | 
| 261 | 
            +
                  feature_flag = @config.get_feature_flag_from_key(feature_flag_key)
         | 
| 262 | 
            +
                  unless feature_flag
         | 
| 263 | 
            +
                    @logger.log(Logger::ERROR, "No feature flag was found for key '#{feature_flag_key}'.")
         | 
| 264 | 
            +
                    return false
         | 
| 265 | 
            +
                  end
         | 
| 266 | 
            +
             | 
| 267 | 
            +
                  decision = @decision_service.get_variation_for_feature(feature_flag, user_id, attributes)
         | 
| 268 | 
            +
                  unless decision.nil?
         | 
| 269 | 
            +
                    variation = decision['variation']
         | 
| 270 | 
            +
                    experiment = decision['experiment']
         | 
| 271 | 
            +
                    unless experiment.nil?
         | 
| 272 | 
            +
                      send_impression(experiment, variation['key'], user_id, attributes)
         | 
| 273 | 
            +
                    else
         | 
| 274 | 
            +
                      @logger.log(Logger::DEBUG,
         | 
| 275 | 
            +
                                  "The user '#{user_id}' is not being experimented on in feature '#{feature_flag_key}'.")
         | 
| 276 | 
            +
                    end
         | 
| 277 | 
            +
             | 
| 278 | 
            +
                    @logger.log(Logger::INFO, "Feature '#{feature_flag_key}' is enabled for user '#{user_id}'.")
         | 
| 279 | 
            +
                    return true
         | 
| 280 | 
            +
                  end
         | 
| 281 | 
            +
             | 
| 282 | 
            +
                  @logger.log(Logger::INFO,
         | 
| 283 | 
            +
                              "Feature '#{feature_flag_key}' is not enabled for user '#{user_id}'.")
         | 
| 284 | 
            +
                  false
         | 
| 285 | 
            +
                end
         | 
| 286 | 
            +
             | 
| 287 | 
            +
                def get_feature_variable_string(feature_flag_key, variable_key, user_id, attributes = nil)
         | 
| 288 | 
            +
                  # Get the String value of the specified variable in the feature flag.
         | 
| 289 | 
            +
                  #
         | 
| 290 | 
            +
                  # feature_flag_key - String key of feature flag the variable belongs to
         | 
| 291 | 
            +
                  # variable_key - String key of variable for which we are getting the string value
         | 
| 292 | 
            +
                  # user_id - String user ID
         | 
| 293 | 
            +
                  # attributes - Hash representing visitor attributes and values which need to be recorded.
         | 
| 294 | 
            +
                  #
         | 
| 295 | 
            +
                  # Returns the string variable value.
         | 
| 296 | 
            +
                  # Returns nil if the feature flag or variable are not found.
         | 
| 297 | 
            +
             | 
| 298 | 
            +
                  variable_value = get_feature_variable_for_type(
         | 
| 299 | 
            +
                    feature_flag_key,
         | 
| 300 | 
            +
                    variable_key,
         | 
| 301 | 
            +
                    Optimizely::Helpers::Constants::VARIABLE_TYPES["STRING"],
         | 
| 302 | 
            +
                    user_id,
         | 
| 303 | 
            +
                    attributes
         | 
| 304 | 
            +
                  )
         | 
| 305 | 
            +
             | 
| 306 | 
            +
                  return variable_value
         | 
| 307 | 
            +
                end
         | 
| 308 | 
            +
             | 
| 309 | 
            +
                def get_feature_variable_boolean(feature_flag_key, variable_key, user_id, attributes = nil)
         | 
| 310 | 
            +
                  # Get the Boolean value of the specified variable in the feature flag.
         | 
| 311 | 
            +
                  #
         | 
| 312 | 
            +
                  # feature_flag_key - String key of feature flag the variable belongs to
         | 
| 313 | 
            +
                  # variable_key - String key of variable for which we are getting the string value
         | 
| 314 | 
            +
                  # user_id - String user ID
         | 
| 315 | 
            +
                  # attributes - Hash representing visitor attributes and values which need to be recorded.
         | 
| 316 | 
            +
                  #
         | 
| 317 | 
            +
                  # Returns the boolean variable value.
         | 
| 318 | 
            +
                  # Returns nil if the feature flag or variable are not found.
         | 
| 319 | 
            +
             | 
| 320 | 
            +
                  variable_value = get_feature_variable_for_type(
         | 
| 321 | 
            +
                    feature_flag_key,
         | 
| 322 | 
            +
                    variable_key,
         | 
| 323 | 
            +
                    Optimizely::Helpers::Constants::VARIABLE_TYPES["BOOLEAN"],
         | 
| 324 | 
            +
                    user_id,
         | 
| 325 | 
            +
                    attributes
         | 
| 326 | 
            +
                  )
         | 
| 327 | 
            +
             | 
| 328 | 
            +
                  return variable_value
         | 
| 329 | 
            +
                end
         | 
| 330 | 
            +
             | 
| 331 | 
            +
                def get_feature_variable_double(feature_flag_key, variable_key, user_id, attributes = nil)
         | 
| 332 | 
            +
                  # Get the Double value of the specified variable in the feature flag.
         | 
| 333 | 
            +
                  #
         | 
| 334 | 
            +
                  # feature_flag_key - String key of feature flag the variable belongs to
         | 
| 335 | 
            +
                  # variable_key - String key of variable for which we are getting the string value
         | 
| 336 | 
            +
                  # user_id - String user ID
         | 
| 337 | 
            +
                  # attributes - Hash representing visitor attributes and values which need to be recorded.
         | 
| 338 | 
            +
                  #
         | 
| 339 | 
            +
                  # Returns the double variable value.
         | 
| 340 | 
            +
                  # Returns nil if the feature flag or variable are not found.
         | 
| 341 | 
            +
             | 
| 342 | 
            +
                  variable_value = get_feature_variable_for_type(
         | 
| 343 | 
            +
                    feature_flag_key,
         | 
| 344 | 
            +
                    variable_key,
         | 
| 345 | 
            +
                    Optimizely::Helpers::Constants::VARIABLE_TYPES["DOUBLE"],
         | 
| 346 | 
            +
                    user_id,
         | 
| 347 | 
            +
                    attributes
         | 
| 348 | 
            +
                  )
         | 
| 349 | 
            +
             | 
| 350 | 
            +
                  return variable_value
         | 
| 351 | 
            +
                end
         | 
| 352 | 
            +
             | 
| 353 | 
            +
                def get_feature_variable_integer(feature_flag_key, variable_key, user_id, attributes = nil)
         | 
| 354 | 
            +
                  # Get the Integer value of the specified variable in the feature flag.
         | 
| 355 | 
            +
                  #
         | 
| 356 | 
            +
                  # feature_flag_key - String key of feature flag the variable belongs to
         | 
| 357 | 
            +
                  # variable_key - String key of variable for which we are getting the string value
         | 
| 358 | 
            +
                  # user_id - String user ID
         | 
| 359 | 
            +
                  # attributes - Hash representing visitor attributes and values which need to be recorded.
         | 
| 360 | 
            +
                  #
         | 
| 361 | 
            +
                  # Returns the integer variable value.
         | 
| 362 | 
            +
                  # Returns nil if the feature flag or variable are not found.
         | 
| 363 | 
            +
             | 
| 364 | 
            +
                  variable_value = get_feature_variable_for_type(
         | 
| 365 | 
            +
                    feature_flag_key,
         | 
| 366 | 
            +
                    variable_key,
         | 
| 367 | 
            +
                    Optimizely::Helpers::Constants::VARIABLE_TYPES["INTEGER"],
         | 
| 368 | 
            +
                    user_id,
         | 
| 369 | 
            +
                    attributes
         | 
| 370 | 
            +
                  )
         | 
| 371 | 
            +
             | 
| 372 | 
            +
                  return variable_value
         | 
| 238 373 | 
             
                end
         | 
| 239 374 |  | 
| 240 375 | 
             
                private
         | 
| 241 376 |  | 
| 377 | 
            +
                def get_feature_variable_for_type(feature_flag_key, variable_key, variable_type, user_id, attributes = nil)
         | 
| 378 | 
            +
                  # Get the variable value for the given feature variable and cast it to the specified type
         | 
| 379 | 
            +
                  # The default value is returned if the feature flag is not enabled for the user.
         | 
| 380 | 
            +
                  #
         | 
| 381 | 
            +
                  # feature_flag_key - String key of feature flag the variable belongs to
         | 
| 382 | 
            +
                  # variable_key - String key of variable for which we are getting the string value
         | 
| 383 | 
            +
                  # variable_type - String requested type for feature variable
         | 
| 384 | 
            +
                  # user_id - String user ID
         | 
| 385 | 
            +
                  # attributes - Hash representing visitor attributes and values which need to be recorded.
         | 
| 386 | 
            +
                  #
         | 
| 387 | 
            +
                  # Returns the type-casted variable value.
         | 
| 388 | 
            +
                  # Returns nil if the feature flag or variable are not found.
         | 
| 389 | 
            +
             | 
| 390 | 
            +
                  feature_flag = @config.get_feature_flag_from_key(feature_flag_key)
         | 
| 391 | 
            +
                  unless feature_flag
         | 
| 392 | 
            +
                    @logger.log(Logger::INFO, "No feature flag was found for key '#{feature_flag_key}'.")
         | 
| 393 | 
            +
                    return nil
         | 
| 394 | 
            +
                  end
         | 
| 395 | 
            +
             | 
| 396 | 
            +
                  variable_value = nil
         | 
| 397 | 
            +
                  variable = @config.get_feature_variable(feature_flag, variable_key)
         | 
| 398 | 
            +
                  unless variable.nil?
         | 
| 399 | 
            +
                    variable_value = variable['defaultValue']
         | 
| 400 | 
            +
             | 
| 401 | 
            +
                    decision = @decision_service.get_variation_for_feature(feature_flag, user_id, attributes)
         | 
| 402 | 
            +
                    unless decision
         | 
| 403 | 
            +
                      @logger.log(Logger::INFO,
         | 
| 404 | 
            +
                        "User '#{user_id}' was not bucketed into any variation for feature flag '#{feature_flag_key}'. Returning the default variable value '#{variable_value}'.")
         | 
| 405 | 
            +
                    else
         | 
| 406 | 
            +
                      variation = decision['variation']
         | 
| 407 | 
            +
                      variation_variable_usages = @config.variation_id_to_variable_usage_map[variation['id']]
         | 
| 408 | 
            +
                      variable_id = variable['id']
         | 
| 409 | 
            +
                      unless variation_variable_usages and variation_variable_usages.key?(variable_id)
         | 
| 410 | 
            +
                        variation_key = variation['key']
         | 
| 411 | 
            +
                        @logger.log(Logger::DEBUG,
         | 
| 412 | 
            +
                          "Variable '#{variable_key}' is not used in variation '#{variation_key}'. Returning the default variable value '#{variable_value}'."
         | 
| 413 | 
            +
                        )
         | 
| 414 | 
            +
                      else
         | 
| 415 | 
            +
                        variable_value = variation_variable_usages[variable_id]['value']
         | 
| 416 | 
            +
                        @logger.log(Logger::INFO,
         | 
| 417 | 
            +
                          "Got variable value '#{variable_value}' for variable '#{variable_key}' of feature flag '#{feature_flag_key}'.")
         | 
| 418 | 
            +
                      end
         | 
| 419 | 
            +
                    end
         | 
| 420 | 
            +
                  end
         | 
| 421 | 
            +
             | 
| 422 | 
            +
                  unless variable_value.nil?
         | 
| 423 | 
            +
                    actual_variable_type = variable['type']
         | 
| 424 | 
            +
                    unless variable_type == actual_variable_type
         | 
| 425 | 
            +
                      @logger.log(Logger::WARN,
         | 
| 426 | 
            +
                        "Requested variable type '#{variable_type}' but variable '#{variable_key}' is of type '#{actual_variable_type}'.")
         | 
| 427 | 
            +
                    end
         | 
| 428 | 
            +
             | 
| 429 | 
            +
                    variable_value = Helpers::VariableType.cast_value_to_type(variable_value, variable_type, @logger)
         | 
| 430 | 
            +
                  end
         | 
| 431 | 
            +
             | 
| 432 | 
            +
                  return variable_value
         | 
| 433 | 
            +
                end
         | 
| 434 | 
            +
             | 
| 242 435 | 
             
                def get_valid_experiments_for_event(event_key, user_id, attributes)
         | 
| 243 436 | 
             
                  # Get the experiments that we should be tracking for the given event.
         | 
| 244 437 | 
             
                  #
         | 
| @@ -313,5 +506,24 @@ module Optimizely | |
| 313 506 | 
             
                  raise InvalidInputError.new('error_handler') unless Helpers::Validator.error_handler_valid?(@error_handler)
         | 
| 314 507 | 
             
                  raise InvalidInputError.new('event_dispatcher') unless Helpers::Validator.event_dispatcher_valid?(@event_dispatcher)
         | 
| 315 508 | 
             
                end
         | 
| 509 | 
            +
             | 
| 510 | 
            +
                def send_impression(experiment, variation_key, user_id, attributes = nil)
         | 
| 511 | 
            +
                  experiment_key = experiment['key']
         | 
| 512 | 
            +
                  variation_id = @config.get_variation_id_from_key(experiment_key, variation_key)
         | 
| 513 | 
            +
                  impression_event = @event_builder.create_impression_event(experiment, variation_id, user_id, attributes)
         | 
| 514 | 
            +
                  @logger.log(Logger::INFO,
         | 
| 515 | 
            +
                              'Dispatching impression event to URL %s with params %s.' % [impression_event.url,
         | 
| 516 | 
            +
                                                                                          impression_event.params])
         | 
| 517 | 
            +
                  begin
         | 
| 518 | 
            +
                    @event_dispatcher.dispatch_event(impression_event)
         | 
| 519 | 
            +
                  rescue => e
         | 
| 520 | 
            +
                    @logger.log(Logger::ERROR, "Unable to dispatch impression event. Error: #{e}")
         | 
| 521 | 
            +
                  end
         | 
| 522 | 
            +
                  variation = @config.get_variation_from_id(experiment_key, variation_id)
         | 
| 523 | 
            +
                  @notification_center.send_notifications(
         | 
| 524 | 
            +
                      NotificationCenter::NOTIFICATION_TYPES[:ACTIVATE],
         | 
| 525 | 
            +
                      experiment,user_id, attributes, variation, impression_event
         | 
| 526 | 
            +
                  )
         | 
| 527 | 
            +
                end
         | 
| 316 528 | 
             
              end
         | 
| 317 529 | 
             
            end
         | 
    
        data/lib/optimizely/bucketer.rb
    CHANGED
    
    | @@ -20,7 +20,7 @@ module Optimizely | |
| 20 20 | 
             
              class Bucketer
         | 
| 21 21 | 
             
                # Optimizely bucketing algorithm that evenly distributes visitors.
         | 
| 22 22 |  | 
| 23 | 
            -
                BUCKETING_ID_TEMPLATE = '%{ | 
| 23 | 
            +
                BUCKETING_ID_TEMPLATE = '%{bucketing_id}%{entity_id}'
         | 
| 24 24 | 
             
                HASH_SEED = 1
         | 
| 25 25 | 
             
                MAX_HASH_VALUE = 2**32
         | 
| 26 26 | 
             
                MAX_TRAFFIC_VALUE = 10_000
         | 
| @@ -35,13 +35,15 @@ module Optimizely | |
| 35 35 | 
             
                  @config = config
         | 
| 36 36 | 
             
                end
         | 
| 37 37 |  | 
| 38 | 
            -
                def bucket(experiment, user_id)
         | 
| 38 | 
            +
                def bucket(experiment, bucketing_id, user_id)
         | 
| 39 39 | 
             
                  # Determines ID of variation to be shown for a given experiment key and user ID.
         | 
| 40 40 | 
             
                  #
         | 
| 41 41 | 
             
                  # experiment - Experiment for which visitor is to be bucketed.
         | 
| 42 | 
            +
                  # bucketing_id - String A customer-assigned value used to generate the bucketing key
         | 
| 42 43 | 
             
                  # user_id - String ID for user.
         | 
| 43 44 | 
             
                  #
         | 
| 44 45 | 
             
                  # Returns variation in which visitor with ID user_id has been placed. Nil if no variation.
         | 
| 46 | 
            +
                  return nil if experiment.nil?
         | 
| 45 47 |  | 
| 46 48 | 
             
                  # check if experiment is in a group; if so, check if user is bucketed into specified experiment
         | 
| 47 49 | 
             
                  experiment_id = experiment['id']
         | 
| @@ -51,7 +53,7 @@ module Optimizely | |
| 51 53 | 
             
                    group = @config.group_key_map.fetch(group_id)
         | 
| 52 54 | 
             
                    if Helpers::Group.random_policy?(group)
         | 
| 53 55 | 
             
                      traffic_allocations = group.fetch('trafficAllocation')
         | 
| 54 | 
            -
                      bucketed_experiment_id = find_bucket(user_id, group_id, traffic_allocations)
         | 
| 56 | 
            +
                      bucketed_experiment_id = find_bucket(bucketing_id, user_id, group_id, traffic_allocations)
         | 
| 55 57 | 
             
                      # return if the user is not bucketed into any experiment
         | 
| 56 58 | 
             
                      unless bucketed_experiment_id
         | 
| 57 59 | 
             
                        @config.logger.log(Logger::INFO, "User '#{user_id}' is in no experiment.")
         | 
| @@ -76,7 +78,7 @@ module Optimizely | |
| 76 78 | 
             
                  end
         | 
| 77 79 |  | 
| 78 80 | 
             
                  traffic_allocations = experiment['trafficAllocation']
         | 
| 79 | 
            -
                  variation_id = find_bucket(user_id, experiment_id, traffic_allocations)
         | 
| 81 | 
            +
                  variation_id = find_bucket(bucketing_id, user_id, experiment_id, traffic_allocations)
         | 
| 80 82 | 
             
                  if variation_id && variation_id != ''
         | 
| 81 83 | 
             
                    variation = @config.get_variation_from_id(experiment_key, variation_id)
         | 
| 82 84 | 
             
                    variation_key = variation ? variation['key'] : nil
         | 
| @@ -96,18 +98,18 @@ module Optimizely | |
| 96 98 | 
             
                  nil
         | 
| 97 99 | 
             
                end
         | 
| 98 100 |  | 
| 99 | 
            -
                def find_bucket(user_id, parent_id, traffic_allocations)
         | 
| 101 | 
            +
                def find_bucket(bucketing_id, user_id, parent_id, traffic_allocations)
         | 
| 100 102 | 
             
                  # Helper function to find the matching entity ID for a given bucketing value in a list of traffic allocations.
         | 
| 101 103 | 
             
                  #
         | 
| 104 | 
            +
                  # bucketing_id - String A customer-assigned value user to generate bucketing key
         | 
| 102 105 | 
             
                  # user_id - String ID for user
         | 
| 103 106 | 
             
                  # parent_id - String entity ID to use for bucketing ID
         | 
| 104 107 | 
             
                  # traffic_allocations - Array of traffic allocations
         | 
| 105 108 | 
             
                  #
         | 
| 106 109 | 
             
                  # Returns entity ID corresponding to the provided bucket value or nil if no match is found.
         | 
| 107 | 
            -
             | 
| 108 | 
            -
                   | 
| 109 | 
            -
                  bucket_value  | 
| 110 | 
            -
                  @config.logger.log(Logger::DEBUG, "Assigned bucket #{bucket_value} to user '#{user_id}'.")
         | 
| 110 | 
            +
                  bucketing_key = sprintf(BUCKETING_ID_TEMPLATE, bucketing_id: bucketing_id, entity_id: parent_id)
         | 
| 111 | 
            +
                  bucket_value = generate_bucket_value(bucketing_key)
         | 
| 112 | 
            +
                  @config.logger.log(Logger::DEBUG, "Assigned bucket #{bucket_value} to user '#{user_id}' with bucketing ID: '#{bucketing_id}'.")
         | 
| 111 113 |  | 
| 112 114 | 
             
                  traffic_allocations.each do |traffic_allocation|
         | 
| 113 115 | 
             
                    current_end_of_range = traffic_allocation['endOfRange']
         | 
| @@ -122,25 +124,25 @@ module Optimizely | |
| 122 124 |  | 
| 123 125 | 
             
                private
         | 
| 124 126 |  | 
| 125 | 
            -
                def generate_bucket_value( | 
| 127 | 
            +
                def generate_bucket_value(bucketing_key)
         | 
| 126 128 | 
             
                  # Helper function to generate bucket value in half-closed interval [0, MAX_TRAFFIC_VALUE).
         | 
| 127 129 | 
             
                  #
         | 
| 128 | 
            -
                  #  | 
| 130 | 
            +
                  # bucketing_key - String - Value used to generate bucket value
         | 
| 129 131 | 
             
                  #
         | 
| 130 | 
            -
                  # Returns bucket value corresponding to the provided bucketing  | 
| 132 | 
            +
                  # Returns bucket value corresponding to the provided bucketing key.
         | 
| 131 133 |  | 
| 132 | 
            -
                  ratio = (generate_unsigned_hash_code_32_bit( | 
| 134 | 
            +
                  ratio = (generate_unsigned_hash_code_32_bit(bucketing_key)).to_f / MAX_HASH_VALUE
         | 
| 133 135 | 
             
                  (ratio * MAX_TRAFFIC_VALUE).to_i
         | 
| 134 136 | 
             
                end
         | 
| 135 137 |  | 
| 136 | 
            -
                def generate_unsigned_hash_code_32_bit( | 
| 138 | 
            +
                def generate_unsigned_hash_code_32_bit(bucketing_key)
         | 
| 137 139 | 
             
                  # Helper function to retreive hash code
         | 
| 138 140 | 
             
                  #
         | 
| 139 | 
            -
                  #  | 
| 141 | 
            +
                  # bucketing_key - String - Value used for the key of the murmur hash
         | 
| 140 142 | 
             
                  #
         | 
| 141 143 | 
             
                  # Returns hash code which is a 32 bit unsigned integer.
         | 
| 142 144 |  | 
| 143 | 
            -
                  MurmurHash3::V32.str_hash( | 
| 145 | 
            +
                  MurmurHash3::V32.str_hash(bucketing_key, @bucket_seed) & UNSIGNED_MAX_32_BIT_VALUE
         | 
| 144 146 | 
             
                end
         | 
| 145 147 | 
             
              end
         | 
| 146 | 
            -
            end
         | 
| 148 | 
            +
            end
         | 
| @@ -16,6 +16,9 @@ | |
| 16 16 | 
             
            require_relative './bucketer'
         | 
| 17 17 |  | 
| 18 18 | 
             
            module Optimizely
         | 
| 19 | 
            +
              
         | 
| 20 | 
            +
              RESERVED_ATTRIBUTE_KEY_BUCKETING_ID = "\$opt_bucketing_id".freeze
         | 
| 21 | 
            +
              
         | 
| 19 22 | 
             
              class DecisionService
         | 
| 20 23 | 
             
                # Optimizely's decision service that determines into which variation of an experiment a user will be allocated.
         | 
| 21 24 | 
             
                #
         | 
| @@ -28,16 +31,16 @@ module Optimizely | |
| 28 31 | 
             
                # 4. Check user profile service for past bucketing decisions (sticky bucketing)
         | 
| 29 32 | 
             
                # 5. Check audience targeting
         | 
| 30 33 | 
             
                # 6. Use Murmurhash3 to bucket the user
         | 
| 31 | 
            -
             | 
| 34 | 
            +
                
         | 
| 32 35 | 
             
                attr_reader :bucketer
         | 
| 33 36 | 
             
                attr_reader :config
         | 
| 34 | 
            -
             | 
| 37 | 
            +
                
         | 
| 35 38 | 
             
                def initialize(config, user_profile_service = nil)
         | 
| 36 39 | 
             
                  @config = config
         | 
| 37 40 | 
             
                  @user_profile_service = user_profile_service
         | 
| 38 41 | 
             
                  @bucketer = Bucketer.new(@config)
         | 
| 39 42 | 
             
                end
         | 
| 40 | 
            -
             | 
| 43 | 
            +
                
         | 
| 41 44 | 
             
                def get_variation(experiment_key, user_id, attributes = nil)
         | 
| 42 45 | 
             
                  # Determines variation into which user will be bucketed.
         | 
| 43 46 | 
             
                  #
         | 
| @@ -46,58 +49,250 @@ module Optimizely | |
| 46 49 | 
             
                  # attributes - Hash representing user attributes
         | 
| 47 50 | 
             
                  #
         | 
| 48 51 | 
             
                  # Returns variation ID where visitor will be bucketed (nil if experiment is inactive or user does not meet audience conditions)
         | 
| 49 | 
            -
             | 
| 52 | 
            +
                  
         | 
| 53 | 
            +
                  # By default, the bucketing ID should be the user ID
         | 
| 54 | 
            +
                  bucketing_id = user_id;
         | 
| 55 | 
            +
                  
         | 
| 56 | 
            +
                  # If the bucketing ID key is defined in attributes, then use that in place of the userID
         | 
| 57 | 
            +
                  if attributes and attributes[RESERVED_ATTRIBUTE_KEY_BUCKETING_ID].is_a? String
         | 
| 58 | 
            +
                    unless attributes[RESERVED_ATTRIBUTE_KEY_BUCKETING_ID].empty?
         | 
| 59 | 
            +
                      bucketing_id = attributes[RESERVED_ATTRIBUTE_KEY_BUCKETING_ID]
         | 
| 60 | 
            +
                      @config.logger.log(Logger::DEBUG, "Setting the bucketing ID '#{bucketing_id}'")
         | 
| 61 | 
            +
                    end
         | 
| 62 | 
            +
                  end
         | 
| 63 | 
            +
                  
         | 
| 50 64 | 
             
                  # Check to make sure experiment is active
         | 
| 51 65 | 
             
                  experiment = @config.get_experiment_from_key(experiment_key)
         | 
| 52 | 
            -
                  if experiment.nil?
         | 
| 53 | 
            -
             | 
| 54 | 
            -
                  end
         | 
| 55 | 
            -
             | 
| 66 | 
            +
                  return nil if experiment.nil?
         | 
| 67 | 
            +
                  
         | 
| 56 68 | 
             
                  experiment_id = experiment['id']
         | 
| 57 69 | 
             
                  unless @config.experiment_running?(experiment)
         | 
| 58 70 | 
             
                    @config.logger.log(Logger::INFO, "Experiment '#{experiment_key}' is not running.")
         | 
| 59 71 | 
             
                    return nil
         | 
| 60 72 | 
             
                  end
         | 
| 61 | 
            -
             | 
| 73 | 
            +
                  
         | 
| 62 74 | 
             
                  # Check if a forced variation is set for the user
         | 
| 63 75 | 
             
                  forced_variation = @config.get_forced_variation(experiment_key, user_id)
         | 
| 64 76 | 
             
                  return forced_variation['id'] if forced_variation
         | 
| 65 | 
            -
             | 
| 77 | 
            +
                  
         | 
| 66 78 | 
             
                  # Check if user is in a white-listed variation
         | 
| 67 79 | 
             
                  whitelisted_variation_id = get_whitelisted_variation_id(experiment_key, user_id)
         | 
| 68 80 | 
             
                  return whitelisted_variation_id if whitelisted_variation_id
         | 
| 69 | 
            -
             | 
| 81 | 
            +
                  
         | 
| 70 82 | 
             
                  # Check for saved bucketing decisions
         | 
| 71 83 | 
             
                  user_profile = get_user_profile(user_id)
         | 
| 72 84 | 
             
                  saved_variation_id = get_saved_variation_id(experiment_id, user_profile)
         | 
| 73 85 | 
             
                  if saved_variation_id
         | 
| 74 86 | 
             
                    @config.logger.log(
         | 
| 75 | 
            -
             | 
| 76 | 
            -
             | 
| 87 | 
            +
                     Logger::INFO,
         | 
| 88 | 
            +
                     "Returning previously activated variation ID #{saved_variation_id} of experiment '#{experiment_key}' for user '#{user_id}' from user profile."
         | 
| 77 89 | 
             
                    )
         | 
| 78 90 | 
             
                    return saved_variation_id
         | 
| 79 91 | 
             
                  end
         | 
| 80 | 
            -
             | 
| 92 | 
            +
                  
         | 
| 81 93 | 
             
                  # Check audience conditions
         | 
| 82 94 | 
             
                  unless Audience.user_in_experiment?(@config, experiment, attributes)
         | 
| 83 95 | 
             
                    @config.logger.log(
         | 
| 84 | 
            -
             | 
| 85 | 
            -
             | 
| 96 | 
            +
                     Logger::INFO,
         | 
| 97 | 
            +
                     "User '#{user_id}' does not meet the conditions to be in experiment '#{experiment_key}'."
         | 
| 86 98 | 
             
                    )
         | 
| 87 99 | 
             
                    return nil
         | 
| 88 100 | 
             
                  end
         | 
| 89 | 
            -
             | 
| 101 | 
            +
                  
         | 
| 90 102 | 
             
                  # Bucket normally
         | 
| 91 | 
            -
                  variation = @bucketer.bucket(experiment, user_id)
         | 
| 103 | 
            +
                  variation = @bucketer.bucket(experiment, bucketing_id, user_id)
         | 
| 92 104 | 
             
                  variation_id = variation ? variation['id'] : nil
         | 
| 93 | 
            -
             | 
| 105 | 
            +
                  
         | 
| 94 106 | 
             
                  # Persist bucketing decision
         | 
| 95 107 | 
             
                  save_user_profile(user_profile, experiment_id, variation_id)
         | 
| 96 108 | 
             
                  variation_id
         | 
| 97 109 | 
             
                end
         | 
| 98 | 
            -
             | 
| 110 | 
            +
                
         | 
| 111 | 
            +
                def get_variation_for_feature(feature_flag, user_id, attributes = nil)
         | 
| 112 | 
            +
                  # Get the variation the user is bucketed into for the given FeatureFlag.
         | 
| 113 | 
            +
                  #
         | 
| 114 | 
            +
                  # feature_flag - The feature flag the user wants to access
         | 
| 115 | 
            +
                  # user_id - String ID for the user
         | 
| 116 | 
            +
                  # attributes - Hash representing user attributes
         | 
| 117 | 
            +
                  #
         | 
| 118 | 
            +
                  # Returns hash with the experiment and variation where visitor will be bucketed (nil if the user is not bucketed into any of the experiments on the feature)
         | 
| 119 | 
            +
                  
         | 
| 120 | 
            +
                  # check if the feature is being experiment on and whether the user is bucketed into the experiment
         | 
| 121 | 
            +
                  decision = get_variation_for_feature_experiment(feature_flag, user_id, attributes)
         | 
| 122 | 
            +
                  return decision unless decision.nil?
         | 
| 123 | 
            +
                  
         | 
| 124 | 
            +
                  feature_flag_key = feature_flag['key']
         | 
| 125 | 
            +
                  variation = get_variation_for_feature_rollout(feature_flag, user_id, attributes)
         | 
| 126 | 
            +
                  if variation
         | 
| 127 | 
            +
                    @config.logger.log(
         | 
| 128 | 
            +
                     Logger::INFO,
         | 
| 129 | 
            +
                     "User '#{user_id}' is in the rollout for feature flag '#{feature_flag_key}'."
         | 
| 130 | 
            +
                    )
         | 
| 131 | 
            +
                    # return decision with nil experiment so we don't track impressions for it
         | 
| 132 | 
            +
                    return {
         | 
| 133 | 
            +
                     'experiment' => nil,
         | 
| 134 | 
            +
                     'variation' => variation
         | 
| 135 | 
            +
                    }
         | 
| 136 | 
            +
                  else
         | 
| 137 | 
            +
                    @config.logger.log(
         | 
| 138 | 
            +
                     Logger::INFO,
         | 
| 139 | 
            +
                     "User '#{user_id}' is not in the rollout for feature flag '#{feature_flag_key}'."
         | 
| 140 | 
            +
                    )
         | 
| 141 | 
            +
                  end
         | 
| 142 | 
            +
                  
         | 
| 143 | 
            +
                  return nil
         | 
| 144 | 
            +
                end
         | 
| 145 | 
            +
                
         | 
| 146 | 
            +
                def get_variation_for_feature_experiment(feature_flag, user_id, attributes = nil)
         | 
| 147 | 
            +
                  # Gets the variation the user is bucketed into for the feature flag's experiment.
         | 
| 148 | 
            +
                  #
         | 
| 149 | 
            +
                  # feature_flag - The feature flag the user wants to access
         | 
| 150 | 
            +
                  # user_id - String ID for the user
         | 
| 151 | 
            +
                  # attributes - Hash representing user attributes
         | 
| 152 | 
            +
                  #
         | 
| 153 | 
            +
                  # Returns a hash with the experiment and variation where visitor will be bucketed
         | 
| 154 | 
            +
                  # or nil if the user is not bucketed into any of the experiments on the feature
         | 
| 155 | 
            +
                  
         | 
| 156 | 
            +
                  feature_flag_key = feature_flag['key']
         | 
| 157 | 
            +
                  unless feature_flag['experimentIds'].empty?
         | 
| 158 | 
            +
                    # check if experiment is part of mutex group
         | 
| 159 | 
            +
                    experiment_id = feature_flag['experimentIds'][0]
         | 
| 160 | 
            +
                    experiment = @config.experiment_id_map[experiment_id]
         | 
| 161 | 
            +
                    unless experiment
         | 
| 162 | 
            +
                      @config.logger.log(
         | 
| 163 | 
            +
                       Logger::DEBUG,
         | 
| 164 | 
            +
                       "Feature flag experiment with ID '#{experiment_id}' is not in the datafile."
         | 
| 165 | 
            +
                      )
         | 
| 166 | 
            +
                      return nil
         | 
| 167 | 
            +
                    end
         | 
| 168 | 
            +
                    
         | 
| 169 | 
            +
                    group_id = experiment['groupId']
         | 
| 170 | 
            +
                    # if experiment is part of mutex group we first determine which experiment (if any) in the group the user is part of
         | 
| 171 | 
            +
                    if group_id and @config.group_key_map.has_key?(group_id)
         | 
| 172 | 
            +
                      group = @config.group_key_map[group_id]
         | 
| 173 | 
            +
                      bucketed_experiment_id = @bucketer.find_bucket(user_id, group_id, group['trafficAllocation'])
         | 
| 174 | 
            +
                      if bucketed_experiment_id.nil?
         | 
| 175 | 
            +
                        @config.logger.log(
         | 
| 176 | 
            +
                         Logger::INFO,
         | 
| 177 | 
            +
                         "The user '#{user_id}' is not bucketed into any of the experiments on the feature '#{feature_flag_key}'."
         | 
| 178 | 
            +
                        )
         | 
| 179 | 
            +
                        return nil
         | 
| 180 | 
            +
                      end
         | 
| 181 | 
            +
                    else
         | 
| 182 | 
            +
                      bucketed_experiment_id = experiment_id
         | 
| 183 | 
            +
                    end
         | 
| 184 | 
            +
                    
         | 
| 185 | 
            +
                    if feature_flag['experimentIds'].include?(bucketed_experiment_id)
         | 
| 186 | 
            +
                      experiment = @config.experiment_id_map[bucketed_experiment_id]
         | 
| 187 | 
            +
                      experiment_key = experiment['key']
         | 
| 188 | 
            +
                      variation_id = get_variation(experiment_key, user_id, attributes)
         | 
| 189 | 
            +
                      unless variation_id.nil?
         | 
| 190 | 
            +
                        variation = @config.variation_id_map[experiment_key][variation_id]
         | 
| 191 | 
            +
                        @config.logger.log(
         | 
| 192 | 
            +
                         Logger::INFO,
         | 
| 193 | 
            +
                         "The user '#{user_id}' is bucketed into experiment '#{experiment_key}' of feature '#{feature_flag_key}'."
         | 
| 194 | 
            +
                        )
         | 
| 195 | 
            +
                        return {
         | 
| 196 | 
            +
                         'variation' => variation,
         | 
| 197 | 
            +
                         'experiment' => experiment
         | 
| 198 | 
            +
                        }
         | 
| 199 | 
            +
                      else
         | 
| 200 | 
            +
                        @config.logger.log(
         | 
| 201 | 
            +
                         Logger::INFO,
         | 
| 202 | 
            +
                         "The user '#{user_id}' is not bucketed into any of the experiments on the feature '#{feature_flag_key}'."
         | 
| 203 | 
            +
                        )
         | 
| 204 | 
            +
                      end
         | 
| 205 | 
            +
                    end
         | 
| 206 | 
            +
                  else
         | 
| 207 | 
            +
                    @config.logger.log(
         | 
| 208 | 
            +
                     Logger::DEBUG,
         | 
| 209 | 
            +
                     "The feature flag '#{feature_flag_key}' is not used in any experiments."
         | 
| 210 | 
            +
                    )
         | 
| 211 | 
            +
                  end
         | 
| 212 | 
            +
                  
         | 
| 213 | 
            +
                  return nil
         | 
| 214 | 
            +
                end
         | 
| 215 | 
            +
                
         | 
| 216 | 
            +
                def get_variation_for_feature_rollout(feature_flag, user_id, attributes = nil)
         | 
| 217 | 
            +
                  # Determine which variation the user is in for a given rollout.
         | 
| 218 | 
            +
                  # Returns the variation of the first experiment the user qualifies for.
         | 
| 219 | 
            +
                  #
         | 
| 220 | 
            +
                  # feature_flag - The feature flag the user wants to access
         | 
| 221 | 
            +
                  # user_id - String ID for the user
         | 
| 222 | 
            +
                  # attributes - Hash representing user attributes
         | 
| 223 | 
            +
                  #
         | 
| 224 | 
            +
                  # Returns the variation the user is bucketed into or nil if not bucketed into any of the targeting rules
         | 
| 225 | 
            +
                  
         | 
| 226 | 
            +
                  rollout_id = feature_flag['rolloutId']
         | 
| 227 | 
            +
                  if rollout_id.nil? or rollout_id.empty?
         | 
| 228 | 
            +
                    feature_flag_key = feature_flag['key']
         | 
| 229 | 
            +
                    @config.logger.log(
         | 
| 230 | 
            +
                     Logger::DEBUG,
         | 
| 231 | 
            +
                     "Feature flag '#{feature_flag_key}' is not part of a rollout."
         | 
| 232 | 
            +
                    )
         | 
| 233 | 
            +
                    return nil
         | 
| 234 | 
            +
                  end
         | 
| 235 | 
            +
                  
         | 
| 236 | 
            +
                  rollout = @config.get_rollout_from_id(rollout_id)
         | 
| 237 | 
            +
                  unless rollout.nil? or rollout['experiments'].empty?
         | 
| 238 | 
            +
                    rollout_experiments = rollout['experiments']
         | 
| 239 | 
            +
                    number_of_rules = rollout_experiments.length - 1
         | 
| 240 | 
            +
                    
         | 
| 241 | 
            +
                    # Go through each experiment in order and try to get the variation for the user
         | 
| 242 | 
            +
                    for index in (0...number_of_rules)
         | 
| 243 | 
            +
                      experiment = rollout_experiments[index]
         | 
| 244 | 
            +
                      experiment_key = experiment['key']
         | 
| 245 | 
            +
                      
         | 
| 246 | 
            +
                      # Check that user meets audience conditions for targeting rule
         | 
| 247 | 
            +
                      unless Audience.user_in_experiment?(@config, experiment, attributes)
         | 
| 248 | 
            +
                        @config.logger.log(
         | 
| 249 | 
            +
                         Logger::DEBUG,
         | 
| 250 | 
            +
                         "User '#{user_id}' does not meet the conditions to be in experiment '#{experiment_key}'."
         | 
| 251 | 
            +
                        )
         | 
| 252 | 
            +
                        # move onto the next targeting rule
         | 
| 253 | 
            +
                        next
         | 
| 254 | 
            +
                      end
         | 
| 255 | 
            +
                      
         | 
| 256 | 
            +
                      @config.logger.log(
         | 
| 257 | 
            +
                       Logger::DEBUG,
         | 
| 258 | 
            +
                       "User '#{user_id}' meets conditions for targeting rule '#{index + 1}'."
         | 
| 259 | 
            +
                      )
         | 
| 260 | 
            +
                      variation = @bucketer.bucket(experiment, user_id)
         | 
| 261 | 
            +
                      unless variation.nil?
         | 
| 262 | 
            +
                        variation_key = variation['key']
         | 
| 263 | 
            +
                        return variation
         | 
| 264 | 
            +
                      end
         | 
| 265 | 
            +
                      
         | 
| 266 | 
            +
                      # User failed traffic allocation, jump to Everyone Else rule
         | 
| 267 | 
            +
                      @config.logger.log(
         | 
| 268 | 
            +
                       Logger::DEBUG,
         | 
| 269 | 
            +
                       "User '#{user_id}' is not in the traffic group for the targeting rule. Checking 'Eveyrone Else' rule now."
         | 
| 270 | 
            +
                      )
         | 
| 271 | 
            +
                      break
         | 
| 272 | 
            +
                    end
         | 
| 273 | 
            +
                    
         | 
| 274 | 
            +
                    # Evalute the "Everyone Else" rule, which is the last rule.
         | 
| 275 | 
            +
                    everyone_else_experiment = rollout_experiments[number_of_rules]
         | 
| 276 | 
            +
                    variation = @bucketer.bucket(everyone_else_experiment, user_id)
         | 
| 277 | 
            +
                    unless variation.nil?
         | 
| 278 | 
            +
                      @config.logger.log(
         | 
| 279 | 
            +
                       Logger::DEBUG,
         | 
| 280 | 
            +
                       "User '#{user_id}' meets conditions for targeting rule 'Everyone Else'."
         | 
| 281 | 
            +
                      )
         | 
| 282 | 
            +
                      return variation
         | 
| 283 | 
            +
                    end
         | 
| 284 | 
            +
                    
         | 
| 285 | 
            +
                    @config.logger.log(
         | 
| 286 | 
            +
                     Logger::DEBUG,
         | 
| 287 | 
            +
                     "User '#{user_id}' does not meet conditions for targeting rule 'Everyone Else'."
         | 
| 288 | 
            +
                    )
         | 
| 289 | 
            +
                  end
         | 
| 290 | 
            +
                  
         | 
| 291 | 
            +
                  return nil
         | 
| 292 | 
            +
                end
         | 
| 293 | 
            +
                
         | 
| 99 294 | 
             
                private
         | 
| 100 | 
            -
             | 
| 295 | 
            +
                
         | 
| 101 296 | 
             
                def get_whitelisted_variation_id(experiment_key, user_id)
         | 
| 102 297 | 
             
                  # Determine if a user is whitelisted into a variation for the given experiment and return the ID of that variation
         | 
| 103 298 | 
             
                  #
         | 
| @@ -105,32 +300,32 @@ module Optimizely | |
| 105 300 | 
             
                  # user_id - ID for the user
         | 
| 106 301 | 
             
                  #
         | 
| 107 302 | 
             
                  # Returns variation ID into which user_id is whitelisted (nil if no variation)
         | 
| 108 | 
            -
             | 
| 303 | 
            +
                  
         | 
| 109 304 | 
             
                  whitelisted_variations = @config.get_whitelisted_variations(experiment_key)
         | 
| 110 | 
            -
             | 
| 305 | 
            +
                  
         | 
| 111 306 | 
             
                  return nil unless whitelisted_variations
         | 
| 112 | 
            -
             | 
| 307 | 
            +
                  
         | 
| 113 308 | 
             
                  whitelisted_variation_key = whitelisted_variations[user_id]
         | 
| 114 | 
            -
             | 
| 309 | 
            +
                  
         | 
| 115 310 | 
             
                  return nil unless whitelisted_variation_key
         | 
| 116 | 
            -
             | 
| 311 | 
            +
                  
         | 
| 117 312 | 
             
                  whitelisted_variation_id = @config.get_variation_id_from_key(experiment_key, whitelisted_variation_key)
         | 
| 118 | 
            -
             | 
| 313 | 
            +
                  
         | 
| 119 314 | 
             
                  unless whitelisted_variation_id
         | 
| 120 315 | 
             
                    @config.logger.log(
         | 
| 121 | 
            -
             | 
| 122 | 
            -
             | 
| 316 | 
            +
                     Logger::INFO,
         | 
| 317 | 
            +
                     "User '#{user_id}' is whitelisted into variation '#{whitelisted_variation_key}', which is not in the datafile."
         | 
| 123 318 | 
             
                    )
         | 
| 124 319 | 
             
                    return nil
         | 
| 125 320 | 
             
                  end
         | 
| 126 | 
            -
             | 
| 321 | 
            +
                  
         | 
| 127 322 | 
             
                  @config.logger.log(
         | 
| 128 | 
            -
             | 
| 129 | 
            -
             | 
| 323 | 
            +
                   Logger::INFO,
         | 
| 324 | 
            +
                   "User '#{user_id}' is whitelisted into variation '#{whitelisted_variation_key}' of experiment '#{experiment_key}'."
         | 
| 130 325 | 
             
                  )
         | 
| 131 326 | 
             
                  whitelisted_variation_id
         | 
| 132 327 | 
             
                end
         | 
| 133 | 
            -
             | 
| 328 | 
            +
                
         | 
| 134 329 | 
             
                def get_saved_variation_id(experiment_id, user_profile)
         | 
| 135 330 | 
             
                  # Retrieve variation ID of stored bucketing decision for a given experiment from a given user profile
         | 
| 136 331 | 
             
                  #
         | 
| @@ -139,56 +334,56 @@ module Optimizely | |
| 139 334 | 
             
                  #
         | 
| 140 335 | 
             
                  # Returns string variation ID (nil if no decision is found)
         | 
| 141 336 | 
             
                  return nil unless user_profile[:experiment_bucket_map]
         | 
| 142 | 
            -
             | 
| 337 | 
            +
                  
         | 
| 143 338 | 
             
                  decision = user_profile[:experiment_bucket_map][experiment_id]
         | 
| 144 339 | 
             
                  return nil unless decision
         | 
| 145 340 | 
             
                  variation_id = decision[:variation_id]
         | 
| 146 341 | 
             
                  return variation_id if @config.variation_id_exists?(experiment_id, variation_id)
         | 
| 147 | 
            -
             | 
| 342 | 
            +
                  
         | 
| 148 343 | 
             
                  @config.logger.log(
         | 
| 149 | 
            -
             | 
| 150 | 
            -
             | 
| 344 | 
            +
                   Logger::INFO,
         | 
| 345 | 
            +
                   "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."
         | 
| 151 346 | 
             
                  )
         | 
| 152 347 | 
             
                  nil
         | 
| 153 348 | 
             
                end
         | 
| 154 | 
            -
             | 
| 349 | 
            +
                
         | 
| 155 350 | 
             
                def get_user_profile(user_id)
         | 
| 156 351 | 
             
                  # Determine if a user is forced into a variation for the given experiment and return the ID of that variation
         | 
| 157 352 | 
             
                  #
         | 
| 158 353 | 
             
                  # user_id - String ID for the user
         | 
| 159 354 | 
             
                  #
         | 
| 160 355 | 
             
                  # Returns Hash stored user profile (or a default one if lookup fails or user profile service not provided)
         | 
| 161 | 
            -
             | 
| 356 | 
            +
                  
         | 
| 162 357 | 
             
                  user_profile = {
         | 
| 163 | 
            -
             | 
| 164 | 
            -
             | 
| 358 | 
            +
                   :user_id => user_id,
         | 
| 359 | 
            +
                   :experiment_bucket_map => {}
         | 
| 165 360 | 
             
                  }
         | 
| 166 | 
            -
             | 
| 361 | 
            +
                  
         | 
| 167 362 | 
             
                  return user_profile unless @user_profile_service
         | 
| 168 | 
            -
             | 
| 363 | 
            +
                  
         | 
| 169 364 | 
             
                  begin
         | 
| 170 365 | 
             
                    user_profile = @user_profile_service.lookup(user_id) || user_profile
         | 
| 171 366 | 
             
                  rescue => e
         | 
| 172 367 | 
             
                    @config.logger.log(Logger::ERROR, "Error while looking up user profile for user ID '#{user_id}': #{e}.")
         | 
| 173 368 | 
             
                  end
         | 
| 174 | 
            -
             | 
| 369 | 
            +
                  
         | 
| 175 370 | 
             
                  user_profile
         | 
| 176 371 | 
             
                end
         | 
| 177 | 
            -
             | 
| 178 | 
            -
             | 
| 372 | 
            +
                
         | 
| 373 | 
            +
                
         | 
| 179 374 | 
             
                def save_user_profile(user_profile, experiment_id, variation_id)
         | 
| 180 375 | 
             
                  # Save a given bucketing decision to a given user profile
         | 
| 181 376 | 
             
                  #
         | 
| 182 377 | 
             
                  # user_profile - Hash user profile
         | 
| 183 378 | 
             
                  # experiment_id - String experiment ID
         | 
| 184 379 | 
             
                  # variation_id - String variation ID
         | 
| 185 | 
            -
             | 
| 380 | 
            +
                  
         | 
| 186 381 | 
             
                  return unless @user_profile_service
         | 
| 187 | 
            -
             | 
| 382 | 
            +
                  
         | 
| 188 383 | 
             
                  user_id = user_profile[:user_id]
         | 
| 189 384 | 
             
                  begin
         | 
| 190 385 | 
             
                    user_profile[:experiment_bucket_map][experiment_id] = {
         | 
| 191 | 
            -
             | 
| 386 | 
            +
                     :variation_id => variation_id
         | 
| 192 387 | 
             
                    }
         | 
| 193 388 | 
             
                    @user_profile_service.save(user_profile)
         | 
| 194 389 | 
             
                    @config.logger.log(Logger::INFO, "Saved variation ID #{variation_id} of experiment ID #{experiment_id} for user '#{user_id}'.")
         | 
| @@ -197,4 +392,4 @@ module Optimizely | |
| 197 392 | 
             
                  end
         | 
| 198 393 | 
             
                end
         | 
| 199 394 | 
             
              end
         | 
| 200 | 
            -
            end
         | 
| 395 | 
            +
            end
         |