amplitude-experiment 1.4.0 → 1.6.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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/amplitude-experiment.gemspec +7 -6
  3. data/lib/amplitude-experiment.rb +16 -2
  4. data/lib/experiment/cohort/cohort.rb +25 -0
  5. data/lib/experiment/cohort/cohort_download_api.rb +90 -0
  6. data/lib/experiment/cohort/cohort_loader.rb +39 -0
  7. data/lib/experiment/cohort/cohort_storage.rb +91 -0
  8. data/lib/experiment/cohort/cohort_sync_config.rb +27 -0
  9. data/lib/experiment/deployment/deployment_runner.rb +135 -0
  10. data/lib/experiment/error.rb +23 -0
  11. data/lib/experiment/evaluation/evaluation.rb +311 -0
  12. data/lib/experiment/evaluation/flag.rb +123 -0
  13. data/lib/experiment/evaluation/murmur3.rb +104 -0
  14. data/lib/experiment/evaluation/select.rb +16 -0
  15. data/lib/experiment/evaluation/semantic_version.rb +52 -0
  16. data/lib/experiment/evaluation/topological_sort.rb +56 -0
  17. data/lib/experiment/{local/fetcher.rb → flag/flag_config_fetcher.rb} +7 -5
  18. data/lib/experiment/flag/flag_config_storage.rb +53 -0
  19. data/lib/experiment/local/client.rb +68 -29
  20. data/lib/experiment/local/config.rb +26 -2
  21. data/lib/experiment/persistent_http_client.rb +1 -1
  22. data/lib/experiment/remote/client.rb +8 -6
  23. data/lib/experiment/remote/config.rb +8 -1
  24. data/lib/experiment/user.rb +40 -20
  25. data/lib/experiment/util/flag_config.rb +60 -0
  26. data/lib/experiment/util/poller.rb +24 -0
  27. data/lib/experiment/util/user.rb +20 -12
  28. data/lib/experiment/version.rb +1 -1
  29. metadata +44 -25
  30. data/lib/experiment/local/evaluation/evaluation.rb +0 -76
  31. data/lib/experiment/local/evaluation/lib/linuxArm64/libevaluation_interop.so +0 -0
  32. data/lib/experiment/local/evaluation/lib/linuxArm64/libevaluation_interop_api.h +0 -110
  33. data/lib/experiment/local/evaluation/lib/linuxX64/libevaluation_interop.so +0 -0
  34. data/lib/experiment/local/evaluation/lib/linuxX64/libevaluation_interop_api.h +0 -110
  35. data/lib/experiment/local/evaluation/lib/macosArm64/libevaluation_interop.dylib +0 -0
  36. data/lib/experiment/local/evaluation/lib/macosArm64/libevaluation_interop_api.h +0 -110
  37. data/lib/experiment/local/evaluation/lib/macosX64/libevaluation_interop.dylib +0 -0
  38. data/lib/experiment/local/evaluation/lib/macosX64/libevaluation_interop_api.h +0 -110
  39. data/lib/experiment/util/topological_sort.rb +0 -39
@@ -0,0 +1,53 @@
1
+ module AmplitudeExperiment
2
+ # FlagConfigStorage
3
+ class FlagConfigStorage
4
+ def flag_config(key)
5
+ raise NotImplementedError
6
+ end
7
+
8
+ def flag_configs
9
+ raise NotImplementedError
10
+ end
11
+
12
+ def put_flag_config(flag_config)
13
+ raise NotImplementedError
14
+ end
15
+
16
+ def remove_if(&condition)
17
+ raise NotImplementedError
18
+ end
19
+ end
20
+
21
+ # InMemoryFlagConfigStorage
22
+ class InMemoryFlagConfigStorage < FlagConfigStorage
23
+ def initialize
24
+ super # Call the parent class's constructor with no arguments
25
+ @flag_configs = {}
26
+ @flag_configs_lock = Mutex.new
27
+ end
28
+
29
+ def flag_config(key)
30
+ @flag_configs_lock.synchronize do
31
+ @flag_configs[key]
32
+ end
33
+ end
34
+
35
+ def flag_configs
36
+ @flag_configs_lock.synchronize do
37
+ @flag_configs.dup
38
+ end
39
+ end
40
+
41
+ def put_flag_config(flag_config)
42
+ @flag_configs_lock.synchronize do
43
+ @flag_configs[flag_config.key] = flag_config
44
+ end
45
+ end
46
+
47
+ def remove_if
48
+ @flag_configs_lock.synchronize do
49
+ @flag_configs.delete_if { |_key, value| yield(value) }
50
+ end
51
+ end
52
+ end
53
+ end
@@ -12,7 +12,6 @@ module AmplitudeExperiment
12
12
  # @param [LocalEvaluationConfig] config The config object
13
13
 
14
14
  def initialize(api_key, config = nil)
15
- require 'experiment/local/evaluation/evaluation'
16
15
  @api_key = api_key
17
16
  @config = config || LocalEvaluationConfig.new
18
17
  @flags = nil
@@ -23,11 +22,26 @@ module AmplitudeExperiment
23
22
  else
24
23
  Logger::INFO
25
24
  end
26
- @fetcher = LocalEvaluationFetcher.new(api_key, @logger, @config.server_url)
27
25
  raise ArgumentError, 'Experiment API key is empty' if @api_key.nil? || @api_key.empty?
28
26
 
27
+ @engine = Evaluation::Engine.new
28
+
29
29
  @assignment_service = nil
30
30
  @assignment_service = AssignmentService.new(AmplitudeAnalytics::Amplitude.new(config.assignment_config.api_key, configuration: config.assignment_config), AssignmentFilter.new(config.assignment_config.cache_capacity)) if config&.assignment_config
31
+
32
+ @cohort_storage = InMemoryCohortStorage.new
33
+ @flag_config_storage = InMemoryFlagConfigStorage.new
34
+ @flag_config_fetcher = LocalEvaluationFetcher.new(@api_key, @logger, @config.server_url)
35
+ @cohort_loader = nil
36
+ unless @config.cohort_sync_config.nil?
37
+ @cohort_download_api = DirectCohortDownloadApi.new(@config.cohort_sync_config.api_key,
38
+ @config.cohort_sync_config.secret_key,
39
+ @config.cohort_sync_config.max_cohort_size,
40
+ @config.cohort_sync_config.cohort_server_url,
41
+ @logger)
42
+ @cohort_loader = CohortLoader.new(@cohort_download_api, @cohort_storage)
43
+ end
44
+ @deployment_runner = DeploymentRunner.new(@config, @flag_config_fetcher, @flag_config_storage, @cohort_storage, @logger, @cohort_loader)
31
45
  end
32
46
 
33
47
  # Locally evaluates flag variants for a user.
@@ -51,19 +65,16 @@ module AmplitudeExperiment
51
65
  # @param [String[]] flag_keys The flags to evaluate with the user, if empty all flags are evaluated
52
66
  # @return [Hash[String, Variant]] The evaluated variants
53
67
  def evaluate_v2(user, flag_keys = [])
54
- flags = @flags_mutex.synchronize do
55
- @flags
56
- end
68
+ flags = @flag_config_storage.flag_configs
57
69
  return {} if flags.nil?
58
70
 
59
- sorted_flags = AmplitudeExperiment.topological_sort(flags, flag_keys.to_set)
60
- flags_json = sorted_flags.to_json
61
-
62
- enriched_user = AmplitudeExperiment.user_to_evaluation_context(user)
63
- user_str = enriched_user.to_json
71
+ sorted_flags = TopologicalSort.sort(flags, flag_keys)
72
+ required_cohorts_in_storage(sorted_flags)
73
+ user = enrich_user_with_cohorts(user, flags) if @config.cohort_sync_config
74
+ context = AmplitudeExperiment.user_to_evaluation_context(user)
64
75
 
65
- @logger.debug("[Experiment] Evaluate: User: #{user_str} - Rules: #{flags}") if @config.debug
66
- result = evaluation(flags_json, user_str)
76
+ @logger.debug("[Experiment] Evaluate: User: #{context} - Rules: #{flags}") if @config.debug
77
+ result = @engine.evaluate(context, sorted_flags)
67
78
  @logger.debug("[Experiment] evaluate - result: #{result}") if @config.debug
68
79
  variants = AmplitudeExperiment.evaluation_variants_json_to_variants(result)
69
80
  @assignment_service&.track(Assignment.new(user, variants))
@@ -76,34 +87,62 @@ module AmplitudeExperiment
76
87
  return if @is_running
77
88
 
78
89
  @logger.debug('[Experiment] poller - start') if @debug
79
- run
90
+ @deployment_runner.start
80
91
  end
81
92
 
82
93
  # Stop polling for flag configurations. Close resource like connection pool with client
83
94
  def stop
84
- @poller_thread&.exit
85
95
  @is_running = false
86
- @poller_thread = nil
96
+ @deployment_runner.stop
87
97
  end
88
98
 
89
99
  private
90
100
 
91
- def run
92
- @is_running = true
93
- begin
94
- flags = @fetcher.fetch_v2
95
- flags_obj = JSON.parse(flags)
96
- flags_map = flags_obj.each_with_object({}) { |flag, hash| hash[flag['key']] = flag }
97
- @flags_mutex.synchronize do
98
- @flags = flags_map
99
- end
100
- rescue StandardError => e
101
- @logger.error("[Experiment] Flag poller - error: #{e.message}")
101
+ def required_cohorts_in_storage(flag_configs)
102
+ stored_cohort_ids = @cohort_storage.cohort_ids
103
+
104
+ flag_configs.each do |flag|
105
+ flag_cohort_ids = AmplitudeExperiment.get_all_cohort_ids_from_flag(flag)
106
+ missing_cohorts = flag_cohort_ids - stored_cohort_ids
107
+
108
+ next unless missing_cohorts.any?
109
+
110
+ # Convert cohort IDs to a comma-separated string
111
+ cohort_ids_str = "[#{flag_cohort_ids.map(&:to_s).join(', ')}]"
112
+ missing_cohorts_str = "[#{missing_cohorts.map(&:to_s).join(', ')}]"
113
+
114
+ message = if @config.cohort_sync_config
115
+ "Evaluating flag #{flag.key} dependent on cohorts #{cohort_ids_str} without #{missing_cohorts_str} in storage"
116
+ else
117
+ "Evaluating flag #{flag.key} dependent on cohorts #{cohort_ids_str} without cohort syncing configured"
118
+ end
119
+
120
+ @logger.warn(message)
121
+ end
122
+ end
123
+
124
+ def enrich_user_with_cohorts(user, flag_configs)
125
+ grouped_cohort_ids = AmplitudeExperiment.get_grouped_cohort_ids_from_flags(flag_configs)
126
+
127
+ if grouped_cohort_ids.key?(USER_GROUP_TYPE)
128
+ user_cohort_ids = grouped_cohort_ids[USER_GROUP_TYPE]
129
+ user.cohort_ids = Array(@cohort_storage.get_cohorts_for_user(user.user_id, user_cohort_ids)) if user_cohort_ids && user.user_id
102
130
  end
103
- @poller_thread = Thread.new do
104
- sleep(@config.flag_config_polling_interval_millis / 1000.to_f)
105
- run
131
+
132
+ user.groups&.each do |group_type, group_names|
133
+ group_name = group_names.first if group_names
134
+ next unless group_name
135
+
136
+ cohort_ids = grouped_cohort_ids[group_type] || []
137
+ next if cohort_ids.empty?
138
+
139
+ user.add_group_cohort_ids(
140
+ group_type,
141
+ group_name,
142
+ Array(@cohort_storage.get_cohorts_for_group(group_type, group_name, cohort_ids))
143
+ )
106
144
  end
145
+ user
107
146
  end
108
147
  end
109
148
  end
@@ -1,8 +1,14 @@
1
1
  module AmplitudeExperiment
2
+ module ServerZone
3
+ US = 'US'.freeze
4
+ EU = 'EU'.freeze
5
+ end
6
+
2
7
  # LocalEvaluationConfig
3
8
  class LocalEvaluationConfig
4
9
  # Default server url
5
10
  DEFAULT_SERVER_URL = 'https://api.lab.amplitude.com'.freeze
11
+ EU_SERVER_URL = 'https://flag.lab.eu.amplitude.com'.freeze
6
12
 
7
13
  # Set to true to log some extra information to the console.
8
14
  # @return [Boolean] the value of debug
@@ -12,6 +18,10 @@ module AmplitudeExperiment
12
18
  # @return [String] the value of server url
13
19
  attr_accessor :server_url
14
20
 
21
+ # Location of the Amplitude data center to get flags and cohorts from, US or EU
22
+ # @return [String] the value of server zone
23
+ attr_accessor :server_zone
24
+
15
25
  # The polling interval for flag configs.
16
26
  # @return [long] the value of flag config polling interval in million seconds
17
27
  attr_accessor :flag_config_polling_interval_millis
@@ -20,14 +30,28 @@ module AmplitudeExperiment
20
30
  # @return [AssignmentConfig] the config instance
21
31
  attr_accessor :assignment_config
22
32
 
33
+ # Configuration for downloading cohorts required for flag evaluation
34
+ # @return [CohortSyncConfig] the config instance
35
+ attr_accessor :cohort_sync_config
36
+
23
37
  # @param [Boolean] debug Set to true to log some extra information to the console.
24
38
  # @param [String] server_url The server endpoint from which to request variants.
39
+ # @param [String] server_zone Location of the Amplitude data center to get flags and cohorts from, US or EU
25
40
  # @param [Hash] bootstrap The value of bootstrap.
26
41
  # @param [long] flag_config_polling_interval_millis The value of flag config polling interval in million seconds.
27
- def initialize(server_url: DEFAULT_SERVER_URL, bootstrap: {},
28
- flag_config_polling_interval_millis: 30_000, debug: false, assignment_config: nil)
42
+ # @param [AssignmentConfig] assignment_config Configuration for automatically tracking assignment events after an evaluation.
43
+ # @param [CohortSyncConfig] cohort_sync_config Configuration for downloading cohorts required for flag evaluation
44
+ def initialize(server_url: DEFAULT_SERVER_URL, server_zone: ServerZone::US, bootstrap: {},
45
+ flag_config_polling_interval_millis: 30_000, debug: false, assignment_config: nil,
46
+ cohort_sync_config: nil)
29
47
  @debug = debug || false
30
48
  @server_url = server_url
49
+ @server_zone = server_zone
50
+ @cohort_sync_config = cohort_sync_config
51
+ if server_url == DEFAULT_SERVER_URL && @server_zone == ServerZone::EU
52
+ @server_url = EU_SERVER_URL
53
+ @cohort_sync_config.cohort_server_url = EU_COHORT_SYNC_URL if @cohort_sync_config && @cohort_sync_config.cohort_server_url == DEFAULT_COHORT_SYNC_URL
54
+ end
31
55
  @bootstrap = bootstrap
32
56
  @flag_config_polling_interval_millis = flag_config_polling_interval_millis
33
57
  @assignment_config = assignment_config
@@ -5,7 +5,7 @@ module AmplitudeExperiment
5
5
  # WARNING: these connections are not safe for concurrent requests. Callers
6
6
  # must synchronize requests per connection.
7
7
  class PersistentHttpClient
8
- DEFAULT_OPTIONS = { read_timeout: 80 }.freeze
8
+ DEFAULT_OPTIONS = { open_timeout: 60, read_timeout: 80 }.freeze
9
9
 
10
10
  class << self
11
11
  # url: URI / String
@@ -89,7 +89,7 @@ module AmplitudeExperiment
89
89
  # @param [User] user
90
90
  def fetch_internal(user)
91
91
  @logger.debug("[Experiment] Fetching variants for user: #{user.as_json}")
92
- do_fetch(user, @config.fetch_timeout_millis)
92
+ do_fetch(user, @config.connect_timeout_millis, @config.fetch_timeout_millis)
93
93
  rescue StandardError => e
94
94
  @logger.error("[Experiment] Fetch failed: #{e.message}")
95
95
  if should_retry_fetch?(e)
@@ -112,7 +112,7 @@ module AmplitudeExperiment
112
112
  @config.fetch_retries.times do
113
113
  sleep(delay_millis.to_f / 1000.0)
114
114
  begin
115
- return do_fetch(user, @config.fetch_retry_timeout_millis)
115
+ return do_fetch(user, @config.connect_timeout_millis, @config.fetch_retry_timeout_millis)
116
116
  rescue StandardError => e
117
117
  @logger.error("[Experiment] Retry failed: #{e.message}")
118
118
  err = e
@@ -123,16 +123,18 @@ module AmplitudeExperiment
123
123
  end
124
124
 
125
125
  # @param [User] user
126
- # @param [Integer] timeout_millis
127
- def do_fetch(user, timeout_millis)
126
+ # @param [Integer] connect_timeout_millis
127
+ # @param [Integer] fetch_timeout_millis
128
+ def do_fetch(user, connect_timeout_millis, fetch_timeout_millis)
128
129
  start_time = Time.now
129
130
  user_context = add_context(user)
130
131
  headers = {
131
132
  'Authorization' => "Api-Key #{@api_key}",
132
133
  'Content-Type' => 'application/json;charset=utf-8'
133
134
  }
134
- read_timeout = timeout_millis.to_f / 1000 if (timeout_millis.to_f / 1000) > 0
135
- http = PersistentHttpClient.get(@uri, { read_timeout: read_timeout }, @api_key)
135
+ connect_timeout = connect_timeout_millis.to_f / 1000 if (connect_timeout_millis.to_f / 1000) > 0
136
+ read_timeout = fetch_timeout_millis.to_f / 1000 if (fetch_timeout_millis.to_f / 1000) > 0
137
+ http = PersistentHttpClient.get(@uri, { open_timeout: connect_timeout, read_timeout: read_timeout }, @api_key)
136
138
  request = Net::HTTP::Post.new(@uri, headers)
137
139
  request.body = user_context.to_json
138
140
  @logger.warn("[Experiment] encoded user object length #{request.body.length} cannot be cached by CDN; must be < 8KB") if request.body.length > 8000
@@ -12,6 +12,10 @@ module AmplitudeExperiment
12
12
  # @return [Boolean] the value of server url
13
13
  attr_accessor :server_url
14
14
 
15
+ # The request connection open timeout, in milliseconds, used when fetching variants triggered by calling start() or setUser().
16
+ # @return [Integer] the value of connect_timeout_millis
17
+ attr_accessor :connect_timeout_millis
18
+
15
19
  # The request timeout, in milliseconds, used when fetching variants triggered by calling start() or setUser().
16
20
  # @return [Integer] the value of fetch_timeout_millis
17
21
  attr_accessor :fetch_timeout_millis
@@ -40,6 +44,8 @@ module AmplitudeExperiment
40
44
 
41
45
  # @param [Boolean] debug Set to true to log some extra information to the console.
42
46
  # @param [String] server_url The server endpoint from which to request variants.
47
+ # @param [Integer] connect_timeout_millis The request connection open timeout, in milliseconds, used when
48
+ # fetching variants triggered by calling start() or setUser().
43
49
  # @param [Integer] fetch_timeout_millis The request timeout, in milliseconds, used when fetching variants
44
50
  # triggered by calling start() or setUser().
45
51
  # @param [Integer] fetch_retries The number of retries to attempt before failing.
@@ -49,11 +55,12 @@ module AmplitudeExperiment
49
55
  # greater than the max, the max is used for all subsequent retries.
50
56
  # @param [Float] fetch_retry_backoff_scalar Scales the minimum backoff exponentially.
51
57
  # @param [Integer] fetch_retry_timeout_millis The request timeout for retrying fetch requests.
52
- def initialize(debug: false, server_url: DEFAULT_SERVER_URL, fetch_timeout_millis: 10_000, fetch_retries: 0,
58
+ def initialize(debug: false, server_url: DEFAULT_SERVER_URL, connect_timeout_millis: 60_000, fetch_timeout_millis: 10_000, fetch_retries: 0,
53
59
  fetch_retry_backoff_min_millis: 500, fetch_retry_backoff_max_millis: 10_000,
54
60
  fetch_retry_backoff_scalar: 1.5, fetch_retry_timeout_millis: 10_000)
55
61
  @debug = debug
56
62
  @server_url = server_url
63
+ @connect_timeout_millis = connect_timeout_millis
57
64
  @fetch_timeout_millis = fetch_timeout_millis
58
65
  @fetch_retries = fetch_retries
59
66
  @fetch_retry_backoff_min_millis = fetch_retry_backoff_min_millis
@@ -80,6 +80,14 @@ module AmplitudeExperiment
80
80
  # @return [Hash, nil] the value of group properties
81
81
  attr_accessor :group_properties
82
82
 
83
+ # Cohort IDs for the user
84
+ # @return [Hash, nil] the value of cohort_ids
85
+ attr_accessor :cohort_ids
86
+
87
+ # Cohort IDs for the user's groups
88
+ # @return [Hash, nil] the value of group_cohort_ids
89
+ attr_accessor :group_cohort_ids
90
+
83
91
  # @param [String, nil] device_id Device ID for associating with an identity in Amplitude
84
92
  # @param [String, nil] user_id User ID for associating with an identity in Amplitude
85
93
  # @param [String, nil] country Predefined field, must be manually provided
@@ -101,7 +109,8 @@ module AmplitudeExperiment
101
109
  # @param [Hash, nil] group_properties Custom properties for groups
102
110
  def initialize(device_id: nil, user_id: nil, country: nil, city: nil, region: nil, dma: nil, ip_address: nil, language: nil,
103
111
  platform: nil, version: nil, os: nil, device_manufacturer: nil, device_brand: nil,
104
- device_model: nil, carrier: nil, library: nil, user_properties: nil, groups: nil, group_properties: nil)
112
+ device_model: nil, carrier: nil, library: nil, user_properties: nil, groups: nil, group_properties: nil,
113
+ cohort_ids: nil, group_cohort_ids: nil)
105
114
  @device_id = device_id
106
115
  @user_id = user_id
107
116
  @country = country
@@ -121,31 +130,35 @@ module AmplitudeExperiment
121
130
  @user_properties = user_properties
122
131
  @groups = groups
123
132
  @group_properties = group_properties
133
+ @cohort_ids = cohort_ids
134
+ @group_cohort_ids = group_cohort_ids
124
135
  end
125
136
 
126
137
  # Return User as Hash.
127
138
  # @return [Hash] Hash object with user values
128
139
  def as_json(_options = {})
129
140
  {
130
- device_id: @device_id,
131
- user_id: @user_id,
132
- country: @country,
133
- city: @city,
134
- region: @region,
135
- dma: @dma,
136
- ip_address: @ip_address,
137
- language: @language,
138
- platform: @platform,
139
- version: @version,
140
- os: @os,
141
- device_manufacturer: @device_manufacturer,
142
- device_brand: @device_brand,
143
- device_model: @device_model,
144
- carrier: @carrier,
145
- library: @library,
146
- user_properties: @user_properties,
147
- groups: @groups,
148
- group_properties: @group_properties
141
+ 'device_id' => @device_id,
142
+ 'user_id' => @user_id,
143
+ 'country' => @country,
144
+ 'city' => @city,
145
+ 'region' => @region,
146
+ 'dma' => @dma,
147
+ 'ip_address' => @ip_address,
148
+ 'language' => @language,
149
+ 'platform' => @platform,
150
+ 'version' => @version,
151
+ 'os' => @os,
152
+ 'device_manufacturer' => @device_manufacturer,
153
+ 'device_brand' => @device_brand,
154
+ 'device_model' => @device_model,
155
+ 'carrier' => @carrier,
156
+ 'library' => @library,
157
+ 'user_properties' => @user_properties,
158
+ 'groups' => @groups,
159
+ 'group_properties' => @group_properties,
160
+ 'cohort_ids' => @cohort_ids,
161
+ 'group_cohort_ids' => @group_cohort_ids
149
162
  }.compact
150
163
  end
151
164
 
@@ -154,5 +167,12 @@ module AmplitudeExperiment
154
167
  def to_json(*options)
155
168
  as_json(*options).to_json(*options)
156
169
  end
170
+
171
+ def add_group_cohort_ids(group_type, group_name, cohort_ids)
172
+ @group_cohort_ids ||= {}
173
+
174
+ group_names = @group_cohort_ids[group_type] ||= {}
175
+ group_names[group_name] = cohort_ids
176
+ end
157
177
  end
158
178
  end
@@ -0,0 +1,60 @@
1
+ module AmplitudeExperiment
2
+ def self.cohort_filter?(condition)
3
+ ['set contains any', 'set does not contain any'].include?(condition.op) &&
4
+ condition.selector &&
5
+ condition.selector[-1] == 'cohort_ids'
6
+ end
7
+
8
+ def self.get_grouped_cohort_condition_ids(segment)
9
+ cohort_ids = {}
10
+ conditions = segment.conditions || []
11
+ conditions.each do |condition|
12
+ condition = condition[0]
13
+ next unless cohort_filter?(condition) && (condition.selector[1].length > 2)
14
+
15
+ context_subtype = condition.selector[1]
16
+ group_type =
17
+ if context_subtype == 'user'
18
+ USER_GROUP_TYPE
19
+ elsif condition.selector.include?('groups')
20
+ condition.selector[2]
21
+ else
22
+ next
23
+ end
24
+ cohort_ids[group_type] ||= Set.new
25
+ cohort_ids[group_type].merge(condition.values)
26
+ end
27
+ cohort_ids
28
+ end
29
+
30
+ def self.get_grouped_cohort_ids_from_flag(flag)
31
+ cohort_ids = {}
32
+ segments = flag.segments || []
33
+ segments.each do |segment|
34
+ get_grouped_cohort_condition_ids(segment).each do |key, values|
35
+ cohort_ids[key] ||= Set.new
36
+ cohort_ids[key].merge(values)
37
+ end
38
+ end
39
+ cohort_ids
40
+ end
41
+
42
+ def self.get_all_cohort_ids_from_flag(flag)
43
+ get_grouped_cohort_ids_from_flag(flag).values.reduce(Set.new) { |acc, set| acc.merge(set) }
44
+ end
45
+
46
+ def self.get_grouped_cohort_ids_from_flags(flags)
47
+ cohort_ids = {}
48
+ flags.each do |_, flag|
49
+ get_grouped_cohort_ids_from_flag(flag).each do |key, values|
50
+ cohort_ids[key] ||= Set.new
51
+ cohort_ids[key].merge(values)
52
+ end
53
+ end
54
+ cohort_ids
55
+ end
56
+
57
+ def self.get_all_cohort_ids_from_flags(flags)
58
+ get_grouped_cohort_ids_from_flags(flags).values.reduce(Set.new) { |acc, set| acc.merge(set) }
59
+ end
60
+ end
@@ -0,0 +1,24 @@
1
+ module AmplitudeExperiment
2
+ # Poller
3
+ class Poller
4
+ def initialize(interval_seconds, callback)
5
+ @interval_seconds = interval_seconds
6
+ @callback = callback
7
+ end
8
+
9
+ def start
10
+ @running = true
11
+ @thread = Thread.new do
12
+ while @running
13
+ @callback.call
14
+ sleep(@interval_seconds)
15
+ end
16
+ end
17
+ end
18
+
19
+ def stop
20
+ @running = false
21
+ @thread&.join
22
+ end
23
+ end
24
+ end
@@ -2,11 +2,13 @@ module AmplitudeExperiment
2
2
  def self.user_to_evaluation_context(user)
3
3
  user_groups = user.groups
4
4
  user_group_properties = user.group_properties
5
+ user_group_cohort_ids = user.group_cohort_ids
5
6
  user_hash = user.as_json.compact
6
- user_hash.delete(:groups)
7
- user_hash.delete(:group_properties)
7
+ user_hash.delete('groups')
8
+ user_hash.delete('group_properties')
9
+ user_hash.delete('group_cohort_ids')
8
10
 
9
- context = user_hash.empty? ? {} : { user: user_hash }
11
+ context = user_hash.empty? ? {} : { 'user' => user_hash }
10
12
 
11
13
  return context if user_groups.nil?
12
14
 
@@ -14,20 +16,26 @@ module AmplitudeExperiment
14
16
  user_groups.each do |group_type, group_name|
15
17
  group_name = group_name[0] if group_name.is_a?(Array) && !group_name.empty?
16
18
 
17
- groups[group_type.to_sym] = { group_name: group_name }
19
+ groups[group_type] = { 'group_name' => group_name }
18
20
 
19
- next if user_group_properties.nil?
21
+ if user_group_properties
22
+ group_properties_type = user_group_properties[group_type]
23
+ if group_properties_type.is_a?(Hash)
24
+ group_properties_name = group_properties_type[group_name]
25
+ groups[group_type]['group_properties'] = group_properties_name if group_properties_name.is_a?(Hash)
26
+ end
27
+ end
20
28
 
21
- group_properties_type = user_group_properties[group_type.to_sym]
22
- next if group_properties_type.nil? || !group_properties_type.is_a?(Hash)
29
+ next unless user_group_cohort_ids
23
30
 
24
- group_properties_name = group_properties_type[group_name.to_sym]
25
- next if group_properties_name.nil? || !group_properties_name.is_a?(Hash)
26
-
27
- groups[group_type.to_sym][:group_properties] = group_properties_name
31
+ group_cohort_ids_type = user_group_cohort_ids[group_type]
32
+ if group_cohort_ids_type.is_a?(Hash)
33
+ group_cohort_ids_name = group_cohort_ids_type[group_name]
34
+ groups[group_type]['cohort_ids'] = group_cohort_ids_name if group_cohort_ids_name.is_a?(Array)
35
+ end
28
36
  end
29
37
 
30
- context[:groups] = groups unless groups.empty?
38
+ context['groups'] = groups unless groups.empty?
31
39
  context
32
40
  end
33
41
  end
@@ -1,3 +1,3 @@
1
1
  module AmplitudeExperiment
2
- VERSION = '1.4.0'.freeze
2
+ VERSION = '1.6.0'.freeze
3
3
  end