amplitude-experiment 1.3.0 → 1.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5bebb227696dfbf198819fdba2640a000c67ca3a9c89d593cc03a4433ef532c8
4
- data.tar.gz: fb79230b6409d82cf3d849f07cb1705abc22ae10a27e7bdfbbe6d1add1e100d0
3
+ metadata.gz: 6df1a01ac720c72256be526321f9b30f93fcdde050ff6a0743c24bd53a64dd5b
4
+ data.tar.gz: d10ebba30ae8a59b7964536909315059c8d096dc1b3a9d0e735c7242db400153
5
5
  SHA512:
6
- metadata.gz: bc2885c715208465d15a6b4217e684a2caa0eb055963b1a5ff0fcdc7829239b894d685f305383cd483fff2b082592c395672c1d75140e0537543b5479e425557
7
- data.tar.gz: 6d1c6b2488ddc5e28d0162e3535be65c3ed3c5767e08af71acdf7933a6f42a20c3d6994638b45713c8f9a9dd20ccfe054a9323465badff1acdd6960fb5cf9376
6
+ metadata.gz: d623457be0fb4cc632f708365d53abbab2b7b647baf7ab37d18d6a05f21e321960e0632152ff2e60686d590e598cfa4333313cb6e31ad29868d2e4798be3e200
7
+ data.tar.gz: e9800dd0da6e0edf6ee8c01bf289700e7f61963fc2e379ea55ddd54dda7c52c8b85f0185c07fcd2a3d2aa8b92391a0bda9828c794f1960d16e39f1e10ed15210
@@ -29,5 +29,5 @@ Gem::Specification.new do |spec|
29
29
  spec.add_development_dependency 'webmock', '~> 3.14'
30
30
  spec.add_development_dependency 'yard', '~> 0.9'
31
31
  spec.metadata['rubygems_mfa_required'] = 'false'
32
- spec.add_runtime_dependency 'ffi', '~> 1.15.5'
32
+ spec.add_runtime_dependency 'ffi', '~> 1.15'
33
33
  end
@@ -15,6 +15,9 @@ require 'experiment/local/assignment/assignment_service'
15
15
  require 'experiment/local/assignment/assignment_config'
16
16
  require 'experiment/util/lru_cache'
17
17
  require 'experiment/util/hash'
18
+ require 'experiment/util/topological_sort'
19
+ require 'experiment/util/user'
20
+ require 'experiment/util/variant'
18
21
  require 'experiment/error'
19
22
 
20
23
  # Amplitude Experiment Module
@@ -8,4 +8,18 @@ module AmplitudeExperiment
8
8
  @status_code = status_code
9
9
  end
10
10
  end
11
+
12
+ class CycleError < StandardError
13
+ # Raised when topological sorting encounters a cycle between flag dependencies.
14
+ attr_reader :path
15
+
16
+ def initialize(path)
17
+ super
18
+ @path = path
19
+ end
20
+
21
+ def to_s
22
+ "Detected a cycle between flags #{@path}"
23
+ end
24
+ end
11
25
  end
@@ -13,7 +13,9 @@ module AmplitudeExperiment
13
13
  def canonicalize
14
14
  sb = "#{@user&.user_id&.strip} #{@user&.device_id&.strip} "
15
15
  results.sort.to_h.each do |key, value|
16
- sb += "#{key.strip} #{value['variant']&.fetch('key', '')&.strip} "
16
+ next unless value.key
17
+
18
+ sb += "#{key.strip} #{value.key&.strip} "
17
19
  end
18
20
  sb
19
21
  end
@@ -8,10 +8,10 @@ module AmplitudeExperiment
8
8
  end
9
9
 
10
10
  def track(assignment)
11
- @amplitude.track(to_event(assignment)) if @assignment_filter.should_track(assignment)
11
+ @amplitude.track(AssignmentService.to_event(assignment)) if @assignment_filter.should_track(assignment)
12
12
  end
13
13
 
14
- def to_event(assignment)
14
+ def self.to_event(assignment)
15
15
  event = AmplitudeAnalytics::BaseEvent.new(
16
16
  '[Experiment] Assignment',
17
17
  user_id: assignment.user.user_id,
@@ -20,28 +20,29 @@ module AmplitudeExperiment
20
20
  user_properties: {}
21
21
  )
22
22
 
23
- assignment.results.each do |results_key, result|
24
- event.event_properties["#{results_key}.variant"] = result['variant']['key']
25
- end
26
-
27
23
  set = {}
28
24
  unset = {}
29
25
 
30
- assignment.results.each do |results_key, result|
31
- next if result['type'] == FLAG_TYPE_MUTUAL_EXCLUSION_GROUP
26
+ assignment.results.sort.to_h.each do |flag_key, variant|
27
+ next unless variant.key
28
+
29
+ version = variant.metadata['flagVersion'] if variant.metadata
30
+ segment_name = variant.metadata['segmentName'] if variant.metadata
31
+ flag_type = variant.metadata['flagType'] if variant.metadata
32
+ default = variant.metadata ? variant.metadata.fetch('default', false) : false
33
+ event.event_properties["#{flag_key}.variant"] = variant.key
34
+ event.event_properties["#{flag_key}.details"] = "v#{version} rule:#{segment_name}" if version && segment_name
35
+ next if flag_type == FLAG_TYPE_MUTUAL_EXCLUSION_GROUP
32
36
 
33
- if result['isDefaultVariant']
34
- unset["[Experiment] #{results_key}"] = '-'
37
+ if default
38
+ unset["[Experiment] #{flag_key}"] = '-'
35
39
  else
36
- set["[Experiment] #{results_key}"] = result['variant']['key']
40
+ set["[Experiment] #{flag_key}"] = variant.key
37
41
  end
38
42
  end
39
-
40
43
  event.user_properties['$set'] = set
41
44
  event.user_properties['$unset'] = unset
42
-
43
45
  event.insert_id = "#{event.user_id} #{event.device_id} #{AmplitudeExperiment.hash_code(assignment.canonicalize)} #{assignment.timestamp / DAY_MILLIS}"
44
-
45
46
  event
46
47
  end
47
48
  end
@@ -3,8 +3,7 @@ require 'logger'
3
3
  require_relative '../../amplitude'
4
4
 
5
5
  module AmplitudeExperiment
6
- FLAG_TYPE_MUTUAL_EXCLUSION_GROUP = 'mutual_exclusion_group'.freeze
7
- FLAG_TYPE_HOLDOUT_GROUP = 'holdout-group'.freeze
6
+ FLAG_TYPE_MUTUAL_EXCLUSION_GROUP = 'mutual-exclusion-group'.freeze
8
7
  # Main client for fetching variant data.
9
8
  class LocalEvaluationClient
10
9
  # Creates a new Experiment Client instance.
@@ -37,18 +36,38 @@ module AmplitudeExperiment
37
36
  # @param [String[]] flag_keys The flags to evaluate with the user. If empty, all flags from the flag cache are evaluated
38
37
  #
39
38
  # @return [Hash[String, Variant]] The evaluated variants
39
+ # @deprecated Please use {evaluate_v2} instead
40
40
  def evaluate(user, flag_keys = [])
41
+ variants = evaluate_v2(user, flag_keys)
42
+ AmplitudeExperiment.filter_default_variants(variants)
43
+ end
44
+
45
+ # Locally evaluates flag variants for a user.
46
+ # This function will only evaluate flags for the keys specified in the flag_keys argument. If flag_keys is
47
+ # missing or None, all flags are evaluated. This function differs from evaluate as it will return a default
48
+ # variant object if the flag was evaluated but the user was not assigned (i.e. off).
49
+ #
50
+ # @param [User] user The user to evaluate
51
+ # @param [String[]] flag_keys The flags to evaluate with the user, if empty all flags are evaluated
52
+ # @return [Hash[String, Variant]] The evaluated variants
53
+ def evaluate_v2(user, flag_keys = [])
41
54
  flags = @flags_mutex.synchronize do
42
55
  @flags
43
56
  end
44
57
  return {} if flags.nil?
45
58
 
46
- user_str = user.to_json
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
47
64
 
48
65
  @logger.debug("[Experiment] Evaluate: User: #{user_str} - Rules: #{flags}") if @config.debug
49
- result = evaluation(flags, user_str)
66
+ result = evaluation(flags_json, user_str)
50
67
  @logger.debug("[Experiment] evaluate - result: #{result}") if @config.debug
51
- parse_results(result, flag_keys, user)
68
+ variants = AmplitudeExperiment.evaluation_variants_json_to_variants(result)
69
+ @assignment_service&.track(Assignment.new(user, variants))
70
+ variants
52
71
  end
53
72
 
54
73
  # Fetch initial flag configurations and start polling for updates.
@@ -69,29 +88,14 @@ module AmplitudeExperiment
69
88
 
70
89
  private
71
90
 
72
- def parse_results(result, flag_keys, user)
73
- variants = {}
74
- assignments = {}
75
- result.each do |key, value|
76
- included = flag_keys.empty? || flag_keys.include?(key)
77
- if !value['isDefaultVariant'] && included
78
- variant_key = value['variant']['key']
79
- variant_payload = value['variant']['payload']
80
- variants.store(key, Variant.new(variant_key, variant_payload))
81
- end
82
-
83
- assignments[key] = value if included || value['type'] == FLAG_TYPE_MUTUAL_EXCLUSION_GROUP || value['type'] == FLAG_TYPE_HOLDOUT_GROUP
84
- end
85
- @assignment_service&.track(Assignment.new(user, assignments))
86
- variants
87
- end
88
-
89
91
  def run
90
92
  @is_running = true
91
93
  begin
92
- flags = @fetcher.fetch_v1
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 }
93
97
  @flags_mutex.synchronize do
94
- @flags = flags
98
+ @flags = flags_map
95
99
  end
96
100
  rescue StandardError => e
97
101
  @logger.error("[Experiment] Flag poller - error: #{e.message}")
@@ -23,7 +23,7 @@ module EvaluationInterop
23
23
 
24
24
  class Libevaluation_interop_ExportedSymbols < FFI::Struct
25
25
  layout :DisposeStablePointer, callback([:pointer], :void),
26
- :DisposeString, callback([:string], :void),
26
+ :DisposeString, callback([:pointer], :void),
27
27
  :IsInstance, callback([:pointer, :string], :pointer),
28
28
  :createNullableByte, callback([:string], :pointer),
29
29
  :getNonNullValueOfByte, callback([:pointer], :pointer),
@@ -57,11 +57,15 @@ module EvaluationInterop
57
57
  attach_function :libevaluation_interop_symbols, [], Libevaluation_interop_ExportedSymbols.by_ref
58
58
  end
59
59
 
60
- def evaluation(rule_json, user_json)
60
+ def evaluation(rule_json, context_json)
61
61
  lib = EvaluationInterop.libevaluation_interop_symbols()
62
- fn = lib[:kotlin][:root][:evaluate]
63
- result_json = fn.call(rule_json, user_json).read_string
62
+ evaluate = lib[:kotlin][:root][:evaluate]
63
+ dispose = lib[:DisposeString]
64
+ result_raw = evaluate.call(rule_json, context_json)
65
+ result_json = result_raw.read_string
64
66
  result = JSON.parse(result_json)
67
+ dispose.call(result_raw)
68
+
65
69
  if result["error"] != nil
66
70
  raise "#{result["error"]}"
67
71
  elsif result["result"] == nil
@@ -99,7 +99,7 @@ typedef struct {
99
99
  /* User functions. */
100
100
  struct {
101
101
  struct {
102
- const char* (*evaluate)(const char* rules, const char* user);
102
+ const char* (*evaluate)(const char* flags, const char* context);
103
103
  } root;
104
104
  } kotlin;
105
105
  } libevaluation_interop_ExportedSymbols;
@@ -99,7 +99,7 @@ typedef struct {
99
99
  /* User functions. */
100
100
  struct {
101
101
  struct {
102
- const char* (*evaluate)(const char* rules, const char* user);
102
+ const char* (*evaluate)(const char* flags, const char* context);
103
103
  } root;
104
104
  } kotlin;
105
105
  } libevaluation_interop_ExportedSymbols;
@@ -99,7 +99,7 @@ typedef struct {
99
99
  /* User functions. */
100
100
  struct {
101
101
  struct {
102
- const char* (*evaluate)(const char* rules, const char* user);
102
+ const char* (*evaluate)(const char* flags, const char* context);
103
103
  } root;
104
104
  } kotlin;
105
105
  } libevaluation_interop_ExportedSymbols;
@@ -99,7 +99,7 @@ typedef struct {
99
99
  /* User functions. */
100
100
  struct {
101
101
  struct {
102
- const char* (*evaluate)(const char* rules, const char* user);
102
+ const char* (*evaluate)(const char* flags, const char* context);
103
103
  } root;
104
104
  } kotlin;
105
105
  } libevaluation_interop_ExportedSymbols;
@@ -31,6 +31,21 @@ module AmplitudeExperiment
31
31
  response.body
32
32
  end
33
33
 
34
+ def fetch_v2
35
+ # fetch flag_configs
36
+ headers = {
37
+ 'Authorization' => "Api-Key #{@api_key}",
38
+ 'Content-Type' => 'application/json;charset=utf-8',
39
+ 'X-Amp-Exp-Library' => "experiment-ruby-server/#{VERSION}"
40
+ }
41
+ request = Net::HTTP::Get.new("#{@server_url}/sdk/v2/flags?v=0", headers)
42
+ response = @http.request(request)
43
+ raise "flagConfigs - received error response: #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPOK)
44
+
45
+ @logger.debug("[Experiment] Fetch flag configs: #{response.body}")
46
+ response.body
47
+ end
48
+
34
49
  # Fetch local evaluation mode flag configs from the Experiment API server.
35
50
  # These flag configs can be used to perform local evaluation.
36
51
  #
@@ -30,7 +30,7 @@ module AmplitudeExperiment
30
30
  # @param [User] user
31
31
  # @return [Hash] Variants Hash
32
32
  def fetch(user)
33
- filter_default_variants(fetch_internal(user))
33
+ AmplitudeExperiment.filter_default_variants(fetch_internal(user))
34
34
  rescue StandardError => e
35
35
  @logger.error("[Experiment] Failed to fetch variants: #{e.message}")
36
36
  {}
@@ -144,30 +144,11 @@ module AmplitudeExperiment
144
144
  raise FetchError.new(response.code.to_i, "Fetch error response: status=#{response.code} #{response.message}") if response.code != '200'
145
145
 
146
146
  json = JSON.parse(response.body)
147
- variants = parse_json_variants(json)
147
+ variants = AmplitudeExperiment.evaluation_variants_json_to_variants(json)
148
148
  @logger.debug("[Experiment] Fetched variants: #{variants}")
149
149
  variants
150
150
  end
151
151
 
152
- # Parse JSON response hash
153
- #
154
- # @param [Hash] json
155
- # @return [Hash] Hash with String => Variant
156
- def parse_json_variants(json)
157
- variants = {}
158
- json.each do |key, value|
159
- variant_value = ''
160
- if value.key?('value')
161
- variant_value = value.fetch('value')
162
- elsif value.key?('key')
163
- # value was previously under the "key" field
164
- variant_value = value.fetch('key')
165
- end
166
- variants.store(key, Variant.new(variant_value, value.fetch('payload', nil), value.fetch('key', nil), value.fetch('metadata', nil)))
167
- end
168
- variants
169
- end
170
-
171
152
  # @param [User] user
172
153
  # @return [User, Hash] user with library context
173
154
  def add_context(user)
@@ -181,18 +162,5 @@ module AmplitudeExperiment
181
162
 
182
163
  true
183
164
  end
184
-
185
- def filter_default_variants(variants)
186
- variants.each do |key, value|
187
- default = value&.metadata&.fetch('default', nil)
188
- deployed = value&.metadata&.fetch('deployed', nil)
189
- default = false if default.nil?
190
- deployed = true if deployed.nil?
191
- variants.delete(key) if default || !deployed
192
- end
193
- variants
194
- end
195
-
196
- private :filter_default_variants
197
165
  end
198
166
  end
@@ -72,6 +72,14 @@ module AmplitudeExperiment
72
72
  # @return [Hash, nil] the value of user properties
73
73
  attr_accessor :user_properties
74
74
 
75
+ # Predefined field, must be manually provided
76
+ # @return [Hash, nil] the value of groups
77
+ attr_accessor :groups
78
+
79
+ # Predefined field, must be manually provided
80
+ # @return [Hash, nil] the value of group properties
81
+ attr_accessor :group_properties
82
+
75
83
  # @param [String, nil] device_id Device ID for associating with an identity in Amplitude
76
84
  # @param [String, nil] user_id User ID for associating with an identity in Amplitude
77
85
  # @param [String, nil] country Predefined field, must be manually provided
@@ -89,9 +97,11 @@ module AmplitudeExperiment
89
97
  # @param [String, nil] carrier Predefined field, must be manually provided
90
98
  # @param [String, nil] library Predefined field, auto populated, can be manually overridden
91
99
  # @param [Hash, nil] user_properties Custom user properties
100
+ # @param [Hash, nil] groups List of groups the user belongs to
101
+ # @param [Hash, nil] group_properties Custom properties for groups
92
102
  def initialize(device_id: nil, user_id: nil, country: nil, city: nil, region: nil, dma: nil, ip_address: nil, language: nil,
93
103
  platform: nil, version: nil, os: nil, device_manufacturer: nil, device_brand: nil,
94
- device_model: nil, carrier: nil, library: nil, user_properties: nil)
104
+ device_model: nil, carrier: nil, library: nil, user_properties: nil, groups: nil, group_properties: nil)
95
105
  @device_id = device_id
96
106
  @user_id = user_id
97
107
  @country = country
@@ -109,6 +119,8 @@ module AmplitudeExperiment
109
119
  @carrier = carrier
110
120
  @library = library
111
121
  @user_properties = user_properties
122
+ @groups = groups
123
+ @group_properties = group_properties
112
124
  end
113
125
 
114
126
  # Return User as Hash.
@@ -131,8 +143,10 @@ module AmplitudeExperiment
131
143
  device_model: @device_model,
132
144
  carrier: @carrier,
133
145
  library: @library,
134
- user_properties: @user_properties
135
- }
146
+ user_properties: @user_properties,
147
+ groups: @groups,
148
+ group_properties: @group_properties
149
+ }.compact
136
150
  end
137
151
 
138
152
  # Return user information as JSON string.
@@ -0,0 +1,39 @@
1
+ module AmplitudeExperiment
2
+ def self.topological_sort(flags, keys = nil, ordered: false)
3
+ available = flags.dup
4
+ result = []
5
+ starting_keys = keys.nil? || keys.empty? ? flags.keys : keys
6
+ # Used for testing to ensure consistency.
7
+ starting_keys.sort! if ordered && (keys.nil? || keys.empty?)
8
+
9
+ starting_keys.each do |flag_key|
10
+ traversal = parent_traversal(flag_key, available, Set.new)
11
+ result.concat(traversal) unless traversal.nil?
12
+ end
13
+ result
14
+ end
15
+
16
+ def self.parent_traversal(flag_key, available, path)
17
+ flag = available[flag_key]
18
+ return nil if flag.nil?
19
+
20
+ dependencies = flag['dependencies']
21
+ if dependencies.nil? || dependencies.empty?
22
+ available.delete(flag_key)
23
+ return [flag]
24
+ end
25
+
26
+ path.add(flag_key)
27
+ result = []
28
+ dependencies.each do |parent_key|
29
+ raise CycleError, path if path.include?(parent_key)
30
+
31
+ traversal = parent_traversal(parent_key, available, path)
32
+ result.concat(traversal) unless traversal.nil?
33
+ end
34
+ result << flag
35
+ path.delete(flag_key)
36
+ available.delete(flag_key)
37
+ result
38
+ end
39
+ end
@@ -0,0 +1,33 @@
1
+ module AmplitudeExperiment
2
+ def self.user_to_evaluation_context(user)
3
+ user_groups = user.groups
4
+ user_group_properties = user.group_properties
5
+ user_hash = user.as_json.compact
6
+ user_hash.delete(:groups)
7
+ user_hash.delete(:group_properties)
8
+
9
+ context = user_hash.empty? ? {} : { user: user_hash }
10
+
11
+ return context if user_groups.nil?
12
+
13
+ groups = {}
14
+ user_groups.each do |group_type, group_name|
15
+ group_name = group_name[0] if group_name.is_a?(Array) && !group_name.empty?
16
+
17
+ groups[group_type.to_sym] = { group_name: group_name }
18
+
19
+ next if user_group_properties.nil?
20
+
21
+ group_properties_type = user_group_properties[group_type.to_sym]
22
+ next if group_properties_type.nil? || !group_properties_type.is_a?(Hash)
23
+
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
28
+ end
29
+
30
+ context[:groups] = groups unless groups.empty?
31
+ context
32
+ end
33
+ end
@@ -0,0 +1,32 @@
1
+ require 'json'
2
+ module AmplitudeExperiment
3
+ def self.evaluation_variants_json_to_variants(variants_json)
4
+ variants = {}
5
+ variants_json.each do |key, value|
6
+ variants[key] = AmplitudeExperiment.evaluation_variant_json_to_variant(value)
7
+ end
8
+ variants
9
+ end
10
+
11
+ def self.evaluation_variant_json_to_variant(variant_json)
12
+ value = variant_json['value']
13
+ value = value.to_json if value && !value.is_a?(String)
14
+ Variant.new(
15
+ value: value,
16
+ key: variant_json['key'],
17
+ payload: variant_json['payload'],
18
+ metadata: variant_json['metadata']
19
+ )
20
+ end
21
+
22
+ def self.filter_default_variants(variants)
23
+ variants.each do |key, value|
24
+ default = value&.metadata&.fetch('default', nil)
25
+ deployed = value&.metadata&.fetch('deployed', nil)
26
+ default = false if default.nil?
27
+ deployed = true if deployed.nil?
28
+ variants.delete(key) if default || !deployed
29
+ end
30
+ variants
31
+ end
32
+ end
@@ -17,7 +17,9 @@ module AmplitudeExperiment
17
17
 
18
18
  # @param [String] value The value of the variant determined by the flag configuration.
19
19
  # @param [Object, nil] payload The attached payload, if any.
20
- def initialize(value, payload = nil, key = nil, metadata = nil)
20
+ # @param [String] key The key of the variant determined by the flag configuration.
21
+ # @param [Object, nil] metadata The attached metadata, if any.
22
+ def initialize(value: nil, payload: nil, key: nil, metadata: nil)
21
23
  @key = key
22
24
  @value = value
23
25
  @payload = payload
@@ -1,3 +1,3 @@
1
1
  module AmplitudeExperiment
2
- VERSION = '1.3.0'.freeze
2
+ VERSION = '1.4.0'.freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: amplitude-experiment
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Amplitude
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-05-17 00:00:00.000000000 Z
11
+ date: 2024-07-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -142,14 +142,14 @@ dependencies:
142
142
  requirements:
143
143
  - - "~>"
144
144
  - !ruby/object:Gem::Version
145
- version: 1.15.5
145
+ version: '1.15'
146
146
  type: :runtime
147
147
  prerelease: false
148
148
  version_requirements: !ruby/object:Gem::Requirement
149
149
  requirements:
150
150
  - - "~>"
151
151
  - !ruby/object:Gem::Version
152
- version: 1.15.5
152
+ version: '1.15'
153
153
  description: Amplitude Experiment Ruby Server SDK
154
154
  email:
155
155
  - sdk@amplitude.com
@@ -200,6 +200,9 @@ files:
200
200
  - lib/experiment/user.rb
201
201
  - lib/experiment/util/hash.rb
202
202
  - lib/experiment/util/lru_cache.rb
203
+ - lib/experiment/util/topological_sort.rb
204
+ - lib/experiment/util/user.rb
205
+ - lib/experiment/util/variant.rb
203
206
  - lib/experiment/variant.rb
204
207
  - lib/experiment/version.rb
205
208
  homepage: https://github.com/amplitude/experiment-ruby-server