class Optimizely::DatafileProjectConfig

Constants

RESERVED_ATTRIBUTE_PREFIX
RUNNING_EXPERIMENT_STATUS

Representation of the Optimizely project config.

Attributes

account_id[R]
anonymize_ip[R]

Boolean - denotes if Optimizely should remove the last block of visitors' IP address before storing event data

attribute_key_map[R]
attributes[R]
audience_id_map[R]
audiences[R]
bot_filtering[R]
datafile[R]
event_key_map[R]
events[R]
experiment_feature_map[R]
experiment_id_map[R]
experiment_key_map[R]
experiments[R]
feature_flag_key_map[R]
feature_flags[R]
feature_variable_key_map[R]
group_id_map[R]
groups[R]
project_id[R]
revision[R]
rollout_experiment_id_map[R]
rollout_id_map[R]
rollouts[R]
send_flag_decisions[R]
typed_audiences[R]
variation_id_map[R]
variation_id_map_by_experiment_id[R]
variation_id_to_variable_usage_map[R]
variation_key_map[R]
variation_key_map_by_experiment_id[R]
version[R]

Public Class Methods

create(datafile, logger, error_handler, skip_json_validation) click to toggle source
# File lib/optimizely/config/datafile_project_config.rb, line 164
def self.create(datafile, logger, error_handler, skip_json_validation)
  # Looks up and sets datafile and config based on response body.
  #
  # datafile - JSON string representing the Optimizely project.
  # logger - Provides a logger instance.
  # error_handler - Provides a handle_error method to handle exceptions.
  # skip_json_validation - Optional boolean param which allows skipping JSON schema
  #                       validation upon object invocation. By default JSON schema validation will be performed.
  # Returns instance of DatafileProjectConfig, nil otherwise.
  if !skip_json_validation && !Helpers::Validator.datafile_valid?(datafile)
    default_logger = SimpleLogger.new
    default_logger.log(Logger::ERROR, InvalidInputError.new('datafile').message)
    return nil
  end

  begin
    config = new(datafile, logger, error_handler)
  rescue StandardError => e
    default_logger = SimpleLogger.new
    error_to_handle = e.class == InvalidDatafileVersionError ? e : InvalidInputError.new('datafile')
    error_msg = error_to_handle.message

    default_logger.log(Logger::ERROR, error_msg)
    error_handler.handle_error error_to_handle
    return nil
  end

  config
end
new(datafile, logger, error_handler) click to toggle source
# File lib/optimizely/config/datafile_project_config.rb, line 62
def initialize(datafile, logger, error_handler)
  # ProjectConfig init method to fetch and set project config data
  #
  # datafile - JSON string representing the project

  config = JSON.parse(datafile)

  @datafile = datafile
  @error_handler = error_handler
  @logger = logger
  @version = config['version']

  raise InvalidDatafileVersionError, @version unless Helpers::Constants::SUPPORTED_VERSIONS.value?(@version)

  @account_id = config['accountId']
  @attributes = config.fetch('attributes', [])
  @audiences = config.fetch('audiences', [])
  @typed_audiences = config.fetch('typedAudiences', [])
  @events = config.fetch('events', [])
  @experiments = config['experiments']
  @feature_flags = config.fetch('featureFlags', [])
  @groups = config.fetch('groups', [])
  @project_id = config['projectId']
  @anonymize_ip = config.key?('anonymizeIP') ? config['anonymizeIP'] : false
  @bot_filtering = config['botFiltering']
  @revision = config['revision']
  @rollouts = config.fetch('rollouts', [])
  @send_flag_decisions = config.fetch('sendFlagDecisions', false)

  # Json type is represented in datafile as a subtype of string for the sake of backwards compatibility.
  # Converting it to a first-class json type while creating Project Config
  @feature_flags.each do |feature_flag|
    feature_flag['variables'].each do |variable|
      if variable['type'] == 'string' && variable['subType'] == 'json'
        variable['type'] = 'json'
        variable.delete('subType')
      end
    end
  end

  # Utility maps for quick lookup
  @attribute_key_map = generate_key_map(@attributes, 'key')
  @event_key_map = generate_key_map(@events, 'key')
  @group_id_map = generate_key_map(@groups, 'id')
  @group_id_map.each do |key, group|
    exps = group.fetch('experiments')
    exps.each do |exp|
      @experiments.push(exp.merge('groupId' => key))
    end
  end
  @experiment_key_map = generate_key_map(@experiments, 'key')
  @experiment_id_map = generate_key_map(@experiments, 'id')
  @audience_id_map = generate_key_map(@audiences, 'id')
  @audience_id_map = @audience_id_map.merge(generate_key_map(@typed_audiences, 'id')) unless @typed_audiences.empty?
  @variation_id_map = {}
  @variation_key_map = {}
  @variation_id_map_by_experiment_id = {}
  @variation_key_map_by_experiment_id = {}
  @variation_id_to_variable_usage_map = {}
  @variation_id_to_experiment_map = {}
  @experiment_id_map.each_value do |exp|
    # Excludes experiments from rollouts
    variations = exp.fetch('variations')
    variations.each do |variation|
      variation_id = variation['id']
      @variation_id_to_experiment_map[variation_id] = exp
    end
  end
  @rollout_id_map = generate_key_map(@rollouts, 'id')
  # split out the experiment key map for rollouts
  @rollout_experiment_id_map = {}
  @rollout_id_map.each_value do |rollout|
    exps = rollout.fetch('experiments')
    @rollout_experiment_id_map = @rollout_experiment_id_map.merge(generate_key_map(exps, 'id'))
  end
  @all_experiments = @experiment_id_map.merge(@rollout_experiment_id_map)
  @all_experiments.each do |id, exp|
    variations = exp.fetch('variations')
    variations.each do |variation|
      variation_id = variation['id']
      variation['featureEnabled'] = variation['featureEnabled'] == true
      variation_variables = variation['variables']
      next if variation_variables.nil?

      @variation_id_to_variable_usage_map[variation_id] = generate_key_map(variation_variables, 'id')
    end
    @variation_id_map[exp['key']] = generate_key_map(variations, 'id')
    @variation_key_map[exp['key']] = generate_key_map(variations, 'key')
    @variation_id_map_by_experiment_id[id] = generate_key_map(variations, 'id')
    @variation_key_map_by_experiment_id[id] = generate_key_map(variations, 'key')
  end
  @feature_flag_key_map = generate_key_map(@feature_flags, 'key')
  @experiment_feature_map = {}
  @feature_variable_key_map = {}
  @feature_flag_key_map.each do |key, feature_flag|
    @feature_variable_key_map[key] = generate_key_map(feature_flag['variables'], 'key')
    feature_flag['experimentIds'].each do |experiment_id|
      @experiment_feature_map[experiment_id] = [feature_flag['id']]
    end
  end
end

Public Instance Methods

experiment_running?(experiment) click to toggle source
# File lib/optimizely/config/datafile_project_config.rb, line 194
def experiment_running?(experiment)
  # Determine if experiment corresponding to given key is running
  #
  # experiment - Experiment
  #
  # Returns true if experiment is running
  RUNNING_EXPERIMENT_STATUS.include?(experiment['status'])
end
feature_experiment?(experiment_id) click to toggle source
# File lib/optimizely/config/datafile_project_config.rb, line 471
def feature_experiment?(experiment_id)
  # Determines if given experiment is a feature test.
  #
  # experiment_id - String experiment ID
  #
  # Returns true if experiment belongs to  any feature,
  #              false otherwise.
  @experiment_feature_map.key?(experiment_id)
end
get_attribute_id(attribute_key) click to toggle source
# File lib/optimizely/config/datafile_project_config.rb, line 384
def get_attribute_id(attribute_key)
  # Get attribute ID for the provided attribute key.
  #
  # Args:
  #   Attribute key for which attribute is to be fetched.
  #
  # Returns:
  #   Attribute ID corresponding to the provided attribute key.
  attribute = @attribute_key_map[attribute_key]
  has_reserved_prefix = attribute_key.to_s.start_with?(RESERVED_ATTRIBUTE_PREFIX)
  unless attribute.nil?
    if has_reserved_prefix
      @logger.log(Logger::WARN, "Attribute '#{attribute_key}' unexpectedly has reserved prefix '#{RESERVED_ATTRIBUTE_PREFIX}'; "\
                  'using attribute ID instead of reserved attribute name.')
    end
    return attribute['id']
  end
  return attribute_key if has_reserved_prefix

  @logger.log Logger::ERROR, "Attribute key '#{attribute_key}' is not in datafile."
  @error_handler.handle_error InvalidAttributeError
  nil
end
get_audience_from_id(audience_id) click to toggle source
# File lib/optimizely/config/datafile_project_config.rb, line 263
def get_audience_from_id(audience_id)
  # Get audience for the provided audience ID
  #
  # audience_id - ID of the audience
  #
  # Returns the audience

  audience = @audience_id_map[audience_id]
  return audience if audience

  @logger.log Logger::ERROR, "Audience '#{audience_id}' is not in datafile."
  @error_handler.handle_error InvalidAudienceError
  nil
end
get_event_from_key(event_key) click to toggle source
# File lib/optimizely/config/datafile_project_config.rb, line 248
def get_event_from_key(event_key)
  # Get event for the provided event key.
  #
  # event_key - Event key for which event is to be determined.
  #
  # Returns Event corresponding to the provided event key.

  event = @event_key_map[event_key]
  return event if event

  @logger.log Logger::ERROR, "Event '#{event_key}' is not in datafile."
  @error_handler.handle_error InvalidEventError
  nil
end
get_experiment_from_id(experiment_id) click to toggle source
# File lib/optimizely/config/datafile_project_config.rb, line 218
def get_experiment_from_id(experiment_id)
  # Retrieves experiment ID for a given key
  #
  # experiment_id - String id representing the experiment
  #
  # Returns Experiment or nil if not found

  experiment = @experiment_id_map[experiment_id]
  return experiment if experiment

  @logger.log Logger::ERROR, "Experiment id '#{experiment_id}' is not in datafile."
  @error_handler.handle_error InvalidExperimentError
  nil
end
get_experiment_from_key(experiment_key) click to toggle source
# File lib/optimizely/config/datafile_project_config.rb, line 203
def get_experiment_from_key(experiment_key)
  # Retrieves experiment ID for a given key
  #
  # experiment_key - String key representing the experiment
  #
  # Returns Experiment or nil if not found

  experiment = @experiment_key_map[experiment_key]
  return experiment if experiment

  @logger.log Logger::ERROR, "Experiment key '#{experiment_key}' is not in datafile."
  @error_handler.handle_error InvalidExperimentError
  nil
end
get_experiment_key(experiment_id) click to toggle source
# File lib/optimizely/config/datafile_project_config.rb, line 233
def get_experiment_key(experiment_id)
  # Retrieves experiment key for a given ID.
  #
  # experiment_id - String ID representing the experiment.
  #
  # Returns String key.

  experiment = @experiment_id_map[experiment_id]
  return experiment['key'] unless experiment.nil?

  @logger.log Logger::ERROR, "Experiment id '#{experiment_id}' is not in datafile."
  @error_handler.handle_error InvalidExperimentError
  nil
end
get_feature_flag_from_key(feature_flag_key) click to toggle source
# File lib/optimizely/config/datafile_project_config.rb, line 429
def get_feature_flag_from_key(feature_flag_key)
  # Retrieves the feature flag with the given key
  #
  # feature_flag_key - String feature key
  #
  # Returns feature flag if found, otherwise nil
  feature_flag = @feature_flag_key_map[feature_flag_key]
  return feature_flag if feature_flag

  @logger.log Logger::ERROR, "Feature flag key '#{feature_flag_key}' is not in datafile."
  nil
end
get_feature_variable(feature_flag, variable_key) click to toggle source
# File lib/optimizely/config/datafile_project_config.rb, line 442
def get_feature_variable(feature_flag, variable_key)
  # Retrieves the variable with the given key for the given feature
  #
  # feature_flag - The feature flag for which we are retrieving the variable
  # variable_key - String variable key
  #
  # Returns variable if found, otherwise nil
  feature_flag_key = feature_flag['key']
  variable = @feature_variable_key_map[feature_flag_key][variable_key]
  return variable if variable

  @logger.log Logger::ERROR, "No feature variable was found for key '#{variable_key}' in feature flag "\
              "'#{feature_flag_key}'."
  nil
end
get_rollout_from_id(rollout_id) click to toggle source
# File lib/optimizely/config/datafile_project_config.rb, line 458
def get_rollout_from_id(rollout_id)
  # Retrieves the rollout with the given ID
  #
  # rollout_id - String rollout ID
  #
  # Returns the rollout if found, otherwise nil
  rollout = @rollout_id_map[rollout_id]
  return rollout if rollout

  @logger.log Logger::ERROR, "Rollout with ID '#{rollout_id}' is not in the datafile."
  nil
end
get_variation_from_id(experiment_key, variation_id) click to toggle source
# File lib/optimizely/config/datafile_project_config.rb, line 278
def get_variation_from_id(experiment_key, variation_id)
  # Get variation given experiment key and variation ID
  #
  # experiment_key - Key representing parent experiment of variation
  # variation_id - ID of the variation
  #
  # Returns the variation or nil if not found

  variation_id_map = @variation_id_map[experiment_key]
  if variation_id_map
    variation = variation_id_map[variation_id]
    return variation if variation

    @logger.log Logger::ERROR, "Variation id '#{variation_id}' is not in datafile."
    @error_handler.handle_error InvalidVariationError
    return nil
  end

  @logger.log Logger::ERROR, "Experiment key '#{experiment_key}' is not in datafile."
  @error_handler.handle_error InvalidExperimentError
  nil
end
get_variation_from_id_by_experiment_id(experiment_id, variation_id) click to toggle source
# File lib/optimizely/config/datafile_project_config.rb, line 301
def get_variation_from_id_by_experiment_id(experiment_id, variation_id)
  # Get variation given experiment ID and variation ID
  #
  # experiment_id - ID representing parent experiment of variation
  # variation_id - ID of the variation
  #
  # Returns the variation or nil if not found

  variation_id_map_by_experiment_id = @variation_id_map_by_experiment_id[experiment_id]
  if variation_id_map_by_experiment_id
    variation = variation_id_map_by_experiment_id[variation_id]
    return variation if variation

    @logger.log Logger::ERROR, "Variation id '#{variation_id}' is not in datafile."
    @error_handler.handle_error InvalidVariationError
    return nil
  end

  @logger.log Logger::ERROR, "Experiment id '#{experiment_id}' is not in datafile."
  @error_handler.handle_error InvalidExperimentError
  nil
end
get_variation_id_from_key(experiment_key, variation_key) click to toggle source
# File lib/optimizely/config/datafile_project_config.rb, line 347
def get_variation_id_from_key(experiment_key, variation_key)
  # Get variation ID given experiment key and variation key
  #
  # experiment_key - Key representing parent experiment of variation
  # variation_key - Key of the variation
  #
  # Returns ID of the variation

  variation_key_map = @variation_key_map[experiment_key]
  if variation_key_map
    variation = variation_key_map[variation_key]
    return variation['id'] if variation

    @logger.log Logger::ERROR, "Variation key '#{variation_key}' is not in datafile."
    @error_handler.handle_error InvalidVariationError
    return nil
  end

  @logger.log Logger::ERROR, "Experiment key '#{experiment_key}' is not in datafile."
  @error_handler.handle_error InvalidExperimentError
  nil
end
get_variation_id_from_key_by_experiment_id(experiment_id, variation_key) click to toggle source
# File lib/optimizely/config/datafile_project_config.rb, line 324
def get_variation_id_from_key_by_experiment_id(experiment_id, variation_key)
  # Get variation given experiment ID and variation key
  #
  # experiment_id - ID representing parent experiment of variation
  # variation_key - Key of the variation
  #
  # Returns the variation or nil if not found

  variation_key_map = @variation_key_map_by_experiment_id[experiment_id]
  if variation_key_map
    variation = variation_key_map[variation_key]
    return variation['id'] if variation

    @logger.log Logger::ERROR, "Variation key '#{variation_key}' is not in datafile."
    @error_handler.handle_error InvalidVariationError
    return nil
  end

  @logger.log Logger::ERROR, "Experiment id '#{experiment_id}' is not in datafile."
  @error_handler.handle_error InvalidExperimentError
  nil
end
get_whitelisted_variations(experiment_id) click to toggle source
# File lib/optimizely/config/datafile_project_config.rb, line 370
def get_whitelisted_variations(experiment_id)
  # Retrieves whitelisted variations for a given experiment id
  #
  # experiment_id - String id representing the experiment
  #
  # Returns whitelisted variations for the experiment or nil

  experiment = @experiment_id_map[experiment_id]
  return experiment['forcedVariations'] if experiment

  @logger.log Logger::ERROR, "Experiment ID '#{experiment_id}' is not in datafile."
  @error_handler.handle_error InvalidExperimentError
end
variation_id_exists?(experiment_id, variation_id) click to toggle source
# File lib/optimizely/config/datafile_project_config.rb, line 408
def variation_id_exists?(experiment_id, variation_id)
  # Determines if a given experiment ID / variation ID pair exists in the datafile
  #
  # experiment_id - String experiment ID
  # variation_id - String variation ID
  #
  # Returns true if variation is in datafile

  experiment_key = get_experiment_key(experiment_id)
  variation_id_map = @variation_id_map[experiment_key]
  if variation_id_map
    variation = variation_id_map[variation_id]
    return true if variation

    @logger.log Logger::ERROR, "Variation ID '#{variation_id}' is not in datafile."
    @error_handler.handle_error InvalidVariationError
  end

  false
end

Private Instance Methods

generate_key_map(array, key) click to toggle source
# File lib/optimizely/config/datafile_project_config.rb, line 483
def generate_key_map(array, key)
  # Helper method to generate map from key to hash in array of hashes
  #
  # array - Array consisting of hash
  # key - Key in each hash which will be key in the map
  #
  # Returns map mapping key to hash

  Hash[array.map { |obj| [obj[key], obj] }]
end