Module: AmplitudeExperiment

Defined in:
lib/experiment/factory.rb,
lib/experiment/user.rb,
lib/experiment/error.rb,
lib/experiment/cookie.rb,
lib/experiment/variant.rb,
lib/experiment/version.rb,
lib/amplitude-experiment.rb,
lib/experiment/util/hash.rb,
lib/experiment/util/user.rb,
lib/experiment/util/poller.rb,
lib/experiment/local/client.rb,
lib/experiment/local/config.rb,
lib/experiment/util/variant.rb,
lib/experiment/cohort/cohort.rb,
lib/experiment/remote/client.rb,
lib/experiment/remote/config.rb,
lib/experiment/util/lru_cache.rb,
lib/experiment/util/flag_config.rb,
lib/experiment/cohort/cohort_loader.rb,
lib/experiment/cohort/cohort_storage.rb,
lib/experiment/util/topological_sort.rb,
lib/experiment/persistent_http_client.rb,
lib/experiment/flag/flag_config_fetcher.rb,
lib/experiment/flag/flag_config_storage.rb,
lib/experiment/cohort/cohort_sync_config.rb,
lib/experiment/cohort/cohort_download_api.rb,
lib/experiment/local/assignment/assignment.rb,
lib/experiment/deployment/deployment_runner.rb,
lib/experiment/local/assignment/assignment_config.rb,
lib/experiment/local/assignment/assignment_filter.rb,
lib/experiment/local/assignment/assignment_service.rb

Overview

AmplitudeExperiment

Defined Under Namespace

Modules: ServerZone Classes: AmplitudeCookie, Assignment, AssignmentConfig, AssignmentFilter, AssignmentService, CacheItem, Cohort, CohortDownloadApi, CohortDownloadError, CohortLoader, CohortStorage, CohortSyncConfig, CohortTooLargeError, CycleError, DeploymentRunner, DirectCohortDownloadApi, FetchError, FlagConfigStorage, HTTPErrorResponseError, InMemoryCohortStorage, InMemoryFlagConfigStorage, LRUCache, ListNode, LocalEvaluationClient, LocalEvaluationConfig, LocalEvaluationFetcher, PersistentHttpClient, Poller, RemoteEvaluationClient, RemoteEvaluationConfig, User, Variant

Constant Summary collapse

VERSION =
'1.5.0'.freeze
FLAG_TYPE_MUTUAL_EXCLUSION_GROUP =
'mutual-exclusion-group'.freeze
USER_GROUP_TYPE =
'User'.freeze
DEFAULT_COHORT_SYNC_URL =
'https://cohort-v2.lab.amplitude.com'.freeze
EU_COHORT_SYNC_URL =
'https://cohort-v2.lab.eu.amplitude.com'.freeze
DAY_MILLIS =
86_400_000

Class Method Summary collapse

Class Method Details

.cohort_filter?(condition) ⇒ Boolean

Returns:

  • (Boolean)


2
3
4
5
6
# File 'lib/experiment/util/flag_config.rb', line 2

def self.cohort_filter?(condition)
  ['set contains any', 'set does not contain any'].include?(condition['op']) &&
    condition['selector'] &&
    condition['selector'][-1] == 'cohort_ids'
end

.evaluation_variant_json_to_variant(variant_json) ⇒ Object



11
12
13
14
15
16
17
18
19
20
# File 'lib/experiment/util/variant.rb', line 11

def self.evaluation_variant_json_to_variant(variant_json)
  value = variant_json['value']
  value = value.to_json if value && !value.is_a?(String)
  Variant.new(
    value: value,
    key: variant_json['key'],
    payload: variant_json['payload'],
    metadata: variant_json['metadata']
  )
end

.evaluation_variants_json_to_variants(variants_json) ⇒ Object



3
4
5
6
7
8
9
# File 'lib/experiment/util/variant.rb', line 3

def self.evaluation_variants_json_to_variants(variants_json)
  variants = {}
  variants_json.each do |key, value|
    variants[key] = AmplitudeExperiment.evaluation_variant_json_to_variant(value)
  end
  variants
end

.filter_default_variants(variants) ⇒ Object



22
23
24
25
26
27
28
29
30
31
# File 'lib/experiment/util/variant.rb', line 22

def self.filter_default_variants(variants)
  variants.each do |key, value|
    default = value&.&.fetch('default', nil)
    deployed = value&.&.fetch('deployed', nil)
    default = false if default.nil?
    deployed = true if deployed.nil?
    variants.delete(key) if default || !deployed
  end
  variants
end

.get_all_cohort_ids_from_flag(flag) ⇒ Object



42
43
44
# File 'lib/experiment/util/flag_config.rb', line 42

def self.get_all_cohort_ids_from_flag(flag)
  get_grouped_cohort_ids_from_flag(flag).values.reduce(Set.new) { |acc, set| acc.merge(set) }
end

.get_all_cohort_ids_from_flags(flags) ⇒ Object



57
58
59
# File 'lib/experiment/util/flag_config.rb', line 57

def self.get_all_cohort_ids_from_flags(flags)
  get_grouped_cohort_ids_from_flags(flags).values.reduce(Set.new) { |acc, set| acc.merge(set) }
end

.get_grouped_cohort_condition_ids(segment) ⇒ Object



8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# File 'lib/experiment/util/flag_config.rb', line 8

def self.get_grouped_cohort_condition_ids(segment)
  cohort_ids = {}
  conditions = segment['conditions'] || []
  conditions.each do |condition|
    condition = condition[0]
    next unless cohort_filter?(condition) && (condition['selector'][1].length > 2)

    context_subtype = condition['selector'][1]
    group_type =
      if context_subtype == 'user'
        USER_GROUP_TYPE
      elsif condition['selector'].include?('groups')
        condition['selector'][2]
      else
        next
      end
    cohort_ids[group_type] ||= Set.new
    cohort_ids[group_type].merge(condition['values'])
  end
  cohort_ids
end

.get_grouped_cohort_ids_from_flag(flag) ⇒ Object



30
31
32
33
34
35
36
37
38
39
40
# File 'lib/experiment/util/flag_config.rb', line 30

def self.get_grouped_cohort_ids_from_flag(flag)
  cohort_ids = {}
  segments = flag['segments'] || []
  segments.each do |segment|
    get_grouped_cohort_condition_ids(segment).each do |key, values|
      cohort_ids[key] ||= Set.new
      cohort_ids[key].merge(values)
    end
  end
  cohort_ids
end

.get_grouped_cohort_ids_from_flags(flags) ⇒ Object



46
47
48
49
50
51
52
53
54
55
# File 'lib/experiment/util/flag_config.rb', line 46

def self.get_grouped_cohort_ids_from_flags(flags)
  cohort_ids = {}
  flags.each do |_, flag|
    get_grouped_cohort_ids_from_flag(flag).each do |key, values|
      cohort_ids[key] ||= Set.new
      cohort_ids[key].merge(values)
    end
  end
  cohort_ids
end

.hash_code(string) ⇒ Object



3
4
5
6
7
8
9
10
11
12
13
14
# File 'lib/experiment/util/hash.rb', line 3

def self.hash_code(string)
  hash = 0
  return hash if string.empty?

  string.each_char do |char|
    chr_code = char.ord
    hash = ((hash << 5) - hash) + chr_code
    hash &= 0xFFFFFFFF
  end

  hash
end

.initialize_local(api_key, config = nil) ⇒ Object

Initializes a local evaluation Client. A local evaluation client can evaluate local flags or experiments for a user without requiring a remote call to the amplitude evaluation server. In order to best leverage local evaluation, all flags, and experiments being evaluated server side should be configured as local.

Parameters:

  • api_key (String)

    The environment API Key

  • config (Config) (defaults to: nil)

    Optional Config.



22
23
24
25
# File 'lib/experiment/factory.rb', line 22

def self.initialize_local(api_key, config = nil)
  @local_instance.store(api_key, LocalEvaluationClient.new(api_key, config)) unless @local_instance.key?(api_key)
  @local_instance.fetch(api_key)
end

.initialize_remote(api_key, config = nil) ⇒ Object

Initializes a singleton Client. This method returns a default singleton instance, subsequent calls to

init will return the initial instance regardless of input.

Parameters:

  • api_key (String)

    The environment API Key

  • config (Config) (defaults to: nil)

    Optional Config.



11
12
13
14
# File 'lib/experiment/factory.rb', line 11

def self.initialize_remote(api_key, config = nil)
  @remote_instance.store(api_key, RemoteEvaluationClient.new(api_key, config)) unless @remote_instance.key?(api_key)
  @remote_instance.fetch(api_key)
end

.parent_traversal(flag_key, available, path) ⇒ Object



16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/experiment/util/topological_sort.rb', line 16

def self.parent_traversal(flag_key, available, path)
  flag = available[flag_key]
  return nil if flag.nil?

  dependencies = flag['dependencies']
  if dependencies.nil? || dependencies.empty?
    available.delete(flag_key)
    return [flag]
  end

  path.add(flag_key)
  result = []
  dependencies.each do |parent_key|
    raise CycleError, path if path.include?(parent_key)

    traversal = parent_traversal(parent_key, available, path)
    result.concat(traversal) unless traversal.nil?
  end
  result << flag
  path.delete(flag_key)
  available.delete(flag_key)
  result
end

.topological_sort(flags, keys = nil, ordered: false) ⇒ Object



2
3
4
5
6
7
8
9
10
11
12
13
14
# File 'lib/experiment/util/topological_sort.rb', line 2

def self.topological_sort(flags, keys = nil, ordered: false)
  available = flags.dup
  result = []
  starting_keys = keys.nil? || keys.empty? ? flags.keys : keys
  # Used for testing to ensure consistency.
  starting_keys.sort! if ordered && (keys.nil? || keys.empty?)

  starting_keys.each do |flag_key|
    traversal = parent_traversal(flag_key, available, Set.new)
    result.concat(traversal) unless traversal.nil?
  end
  result
end

.user_to_evaluation_context(user) ⇒ Object



2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/experiment/util/user.rb', line 2

def self.user_to_evaluation_context(user)
  user_groups = user.groups
  user_group_properties = user.group_properties
  user_group_cohort_ids = user.group_cohort_ids
  user_hash = user.as_json.compact
  user_hash.delete('groups')
  user_hash.delete('group_properties')
  user_hash.delete('group_cohort_ids')

  context = user_hash.empty? ? {} : { 'user' => user_hash }

  return context if user_groups.nil?

  groups = {}
  user_groups.each do |group_type, group_name|
    group_name = group_name[0] if group_name.is_a?(Array) && !group_name.empty?

    groups[group_type] = { 'group_name' => group_name }

    if user_group_properties
      group_properties_type = user_group_properties[group_type]
      if group_properties_type.is_a?(Hash)
        group_properties_name = group_properties_type[group_name]
        groups[group_type]['group_properties'] = group_properties_name if group_properties_name.is_a?(Hash)
      end
    end

    next unless user_group_cohort_ids

    group_cohort_ids_type = user_group_cohort_ids[group_type]
    if group_cohort_ids_type.is_a?(Hash)
      group_cohort_ids_name = group_cohort_ids_type[group_name]
      groups[group_type]['cohort_ids'] = group_cohort_ids_name if group_cohort_ids_name.is_a?(Array)
    end
  end

  context['groups'] = groups unless groups.empty?
  context
end