amplitude-experiment 1.7.0 → 1.7.1

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: c510533f1277e5400d2b4b82e43703668676cef6f0077c0894e8f7e502f112a0
4
- data.tar.gz: cfe5c55993a65b66807ad3c67f0856f077fc0b7c70a9ac7a69ee0b6d26e52b72
3
+ metadata.gz: 6562a2b13fb38d240e3d83762e442a558478db59f3fff36bdff10b6589cfab0d
4
+ data.tar.gz: 432ea0ff64724c882ad97aae3ce95ae8d8a203f9c90bfe34afd5629e00dbba28
5
5
  SHA512:
6
- metadata.gz: 34857d29d97eadbe26fa887fe26b4ee75f7ca358a9508dc5af3221af184e516e027f467d1726ec67734f842b5dd9e9dded521245aaed9e8c2d1f84a2e99708c2
7
- data.tar.gz: 9cde9399703105c0b971a3596e58a817c10046dce2ce52468286608bf3bfc4ef9428710be0bc0a8e706bda692a185e44ce1cd5d2965edd786542ed07454d6972
6
+ metadata.gz: 507368b7f4db8a28dfc866e0df203f724a1d2911af19dab24fada0bb7327e362d185057c891d534421c7816ec12953ad6555a0252b9bfa7d4922ffaea16bc935
7
+ data.tar.gz: d7ad16548702c918919a9d3b4afe874d396c8c5e98fb5010ebb3f8c0f0b476ad86d4568ed4660a74040a5ee85d4a9e01197560039b19d94659ec0e38d67b0868
@@ -18,7 +18,7 @@ Gem::Specification.new do |spec|
18
18
  spec.require_paths = ['lib']
19
19
  spec.extra_rdoc_files = ['README.md']
20
20
 
21
- spec.add_dependency 'concurrent-ruby', '~> 1.2.2'
21
+ spec.add_dependency 'concurrent-ruby', '~> 1.2'
22
22
 
23
23
  spec.add_development_dependency 'psych', '~> 4.0'
24
24
  spec.add_development_dependency 'rake', '~> 13.0'
@@ -27,12 +27,7 @@ require 'experiment/cohort/cohort_storage'
27
27
  require 'experiment/cohort/cohort_sync_config'
28
28
  require 'experiment/deployment/deployment_runner'
29
29
  require 'experiment/util/poller'
30
- require 'experiment/evaluation/evaluation'
31
- require 'experiment/evaluation/flag'
32
- require 'experiment/evaluation/murmur3'
33
- require 'experiment/evaluation/select'
34
- require 'experiment/evaluation/semantic_version'
35
- require 'experiment/evaluation/topological_sort'
30
+ require 'experiment/evaluation'
36
31
 
37
32
  # Amplitude Experiment Module
38
33
  module AmplitudeExperiment
@@ -1,311 +1,313 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Evaluation
4
- # Engine for evaluating feature flags based on context
5
- class Engine
6
- def evaluate(context, flags)
7
- results = {}
8
- target = {
9
- 'context' => context,
10
- 'result' => results
11
- }
12
-
13
- flags.each do |flag|
14
- variant = evaluate_flag(target, flag)
15
- results[flag.key] = variant if variant
16
- end
3
+ module AmplitudeExperiment
4
+ module Evaluation
5
+ # Engine for evaluating feature flags based on context
6
+ class Engine
7
+ def evaluate(context, flags)
8
+ results = {}
9
+ target = {
10
+ 'context' => context,
11
+ 'result' => results
12
+ }
13
+
14
+ flags.each do |flag|
15
+ variant = evaluate_flag(target, flag)
16
+ results[flag.key] = variant if variant
17
+ end
17
18
 
18
- results
19
- end
19
+ results
20
+ end
20
21
 
21
- private
22
-
23
- def evaluate_flag(target, flag)
24
- result = nil
25
- flag.segments.each do |segment|
26
- result = evaluate_segment(target, flag, segment)
27
- next unless result
28
-
29
- # Merge all metadata into the result
30
- metadata = {}
31
- metadata.merge!(flag.metadata) if flag.metadata
32
- metadata.merge!(segment.metadata) if segment.metadata
33
- metadata.merge!(result.metadata) if result.metadata
34
- result.metadata = metadata
35
- break
22
+ private
23
+
24
+ def evaluate_flag(target, flag)
25
+ result = nil
26
+ flag.segments.each do |segment|
27
+ result = evaluate_segment(target, flag, segment)
28
+ next unless result
29
+
30
+ # Merge all metadata into the result
31
+ metadata = {}
32
+ metadata.merge!(flag.metadata) if flag.metadata
33
+ metadata.merge!(segment.metadata) if segment.metadata
34
+ metadata.merge!(result.metadata) if result.metadata
35
+ result.metadata = metadata
36
+ break
37
+ end
38
+ result
36
39
  end
37
- result
38
- end
39
40
 
40
- def evaluate_segment(target, flag, segment)
41
- if segment.conditions
42
- match = evaluate_conditions(target, segment.conditions)
43
- if match
41
+ def evaluate_segment(target, flag, segment)
42
+ if segment.conditions
43
+ match = evaluate_conditions(target, segment.conditions)
44
+ if match
45
+ variant_key = bucket(target, segment)
46
+ variant_key ? flag.variants[variant_key] : nil
47
+ end
48
+ else
49
+ # Null conditions always match
44
50
  variant_key = bucket(target, segment)
45
51
  variant_key ? flag.variants[variant_key] : nil
46
52
  end
47
- else
48
- # Null conditions always match
49
- variant_key = bucket(target, segment)
50
- variant_key ? flag.variants[variant_key] : nil
51
53
  end
52
- end
53
54
 
54
- def evaluate_conditions(target, conditions)
55
- # Outer list logic is "or" (||)
56
- conditions.any? do |inner_conditions|
57
- match = true
58
- inner_conditions.each do |condition|
59
- match = match_condition(target, condition)
60
- break unless match
55
+ def evaluate_conditions(target, conditions)
56
+ # Outer list logic is "or" (||)
57
+ conditions.any? do |inner_conditions|
58
+ match = true
59
+ inner_conditions.each do |condition|
60
+ match = match_condition(target, condition)
61
+ break unless match
62
+ end
63
+ match
61
64
  end
62
- match
63
65
  end
64
- end
65
66
 
66
- def match_condition(target, condition)
67
- prop_value = Evaluation.select(target, condition.selector)
68
- # Special matching for null properties and set type prop values and operators
69
- if !prop_value
70
- match_null(condition.op, condition.values)
71
- elsif set_operator?(condition.op)
72
- prop_value_string_list = coerce_string_array(prop_value)
73
- return false unless prop_value_string_list
74
-
75
- match_set(prop_value_string_list, condition.op, condition.values)
76
- else
77
- prop_value_string = coerce_string(prop_value)
78
- if prop_value_string
79
- match_string(prop_value_string, condition.op, condition.values)
67
+ def match_condition(target, condition)
68
+ prop_value = Evaluation.select(target, condition.selector)
69
+ # Special matching for null properties and set type prop values and operators
70
+ if !prop_value
71
+ match_null(condition.op, condition.values)
72
+ elsif set_operator?(condition.op)
73
+ prop_value_string_list = coerce_string_array(prop_value)
74
+ return false unless prop_value_string_list
75
+
76
+ match_set(prop_value_string_list, condition.op, condition.values)
80
77
  else
81
- false
78
+ prop_value_string = coerce_string(prop_value)
79
+ if prop_value_string
80
+ match_string(prop_value_string, condition.op, condition.values)
81
+ else
82
+ false
83
+ end
82
84
  end
83
85
  end
84
- end
85
-
86
- def get_hash(key)
87
- Murmur3.hash32x86(key)
88
- end
89
86
 
90
- def bucket(target, segment)
91
- unless segment.bucket
92
- # Null bucket means segment is fully rolled out
93
- return segment.variant
87
+ def get_hash(key)
88
+ Murmur3.hash32x86(key)
94
89
  end
95
90
 
96
- bucketing_value = coerce_string(Evaluation.select(target, segment.bucket.selector))
97
- if !bucketing_value || bucketing_value.empty?
98
- # Null or empty bucketing value cannot be bucketed
99
- return segment.variant
100
- end
91
+ def bucket(target, segment)
92
+ unless segment.bucket
93
+ # Null bucket means segment is fully rolled out
94
+ return segment.variant
95
+ end
101
96
 
102
- key_to_hash = "#{segment.bucket.salt}/#{bucketing_value}"
103
- hash = get_hash(key_to_hash)
104
- allocation_value = hash % 100
105
- distribution_value = (hash / 100).floor
97
+ bucketing_value = coerce_string(Evaluation.select(target, segment.bucket.selector))
98
+ if !bucketing_value || bucketing_value.empty?
99
+ # Null or empty bucketing value cannot be bucketed
100
+ return segment.variant
101
+ end
102
+
103
+ key_to_hash = "#{segment.bucket.salt}/#{bucketing_value}"
104
+ hash = get_hash(key_to_hash)
105
+ allocation_value = hash % 100
106
+ distribution_value = (hash / 100).floor
106
107
 
107
- segment.bucket.allocations.each do |allocation|
108
- allocation_start = allocation.range[0]
109
- allocation_end = allocation.range[1]
110
- next unless allocation_value >= allocation_start && allocation_value < allocation_end
108
+ segment.bucket.allocations.each do |allocation|
109
+ allocation_start = allocation.range[0]
110
+ allocation_end = allocation.range[1]
111
+ next unless allocation_value >= allocation_start && allocation_value < allocation_end
111
112
 
112
- allocation.distributions.each do |distribution|
113
- distribution_start = distribution.range[0]
114
- distribution_end = distribution.range[1]
115
- return distribution.variant if distribution_value >= distribution_start && distribution_value < distribution_end
113
+ allocation.distributions.each do |distribution|
114
+ distribution_start = distribution.range[0]
115
+ distribution_end = distribution.range[1]
116
+ return distribution.variant if distribution_value >= distribution_start && distribution_value < distribution_end
117
+ end
116
118
  end
117
- end
118
119
 
119
- segment.variant
120
- end
120
+ segment.variant
121
+ end
121
122
 
122
- def match_null(op, filter_values)
123
- contains_none = contains_none?(filter_values)
124
- case op
125
- when Operator::IS, Operator::CONTAINS, Operator::LESS_THAN,
126
- Operator::LESS_THAN_EQUALS, Operator::GREATER_THAN,
127
- Operator::GREATER_THAN_EQUALS, Operator::VERSION_LESS_THAN,
128
- Operator::VERSION_LESS_THAN_EQUALS, Operator::VERSION_GREATER_THAN,
129
- Operator::VERSION_GREATER_THAN_EQUALS, Operator::SET_IS,
130
- Operator::SET_CONTAINS, Operator::SET_CONTAINS_ANY
131
- contains_none
132
- when Operator::IS_NOT, Operator::DOES_NOT_CONTAIN,
133
- Operator::SET_DOES_NOT_CONTAIN, Operator::SET_DOES_NOT_CONTAIN_ANY
134
- !contains_none
135
- else
136
- false
123
+ def match_null(op, filter_values)
124
+ contains_none = contains_none?(filter_values)
125
+ case op
126
+ when Operator::IS, Operator::CONTAINS, Operator::LESS_THAN,
127
+ Operator::LESS_THAN_EQUALS, Operator::GREATER_THAN,
128
+ Operator::GREATER_THAN_EQUALS, Operator::VERSION_LESS_THAN,
129
+ Operator::VERSION_LESS_THAN_EQUALS, Operator::VERSION_GREATER_THAN,
130
+ Operator::VERSION_GREATER_THAN_EQUALS, Operator::SET_IS,
131
+ Operator::SET_CONTAINS, Operator::SET_CONTAINS_ANY
132
+ contains_none
133
+ when Operator::IS_NOT, Operator::DOES_NOT_CONTAIN,
134
+ Operator::SET_DOES_NOT_CONTAIN, Operator::SET_DOES_NOT_CONTAIN_ANY
135
+ !contains_none
136
+ else
137
+ false
138
+ end
137
139
  end
138
- end
139
140
 
140
- def match_set(prop_values, op, filter_values)
141
- case op
142
- when Operator::SET_IS
143
- set_equals?(prop_values, filter_values)
144
- when Operator::SET_IS_NOT
145
- !set_equals?(prop_values, filter_values)
146
- when Operator::SET_CONTAINS
147
- matches_set_contains_all?(prop_values, filter_values)
148
- when Operator::SET_DOES_NOT_CONTAIN
149
- !matches_set_contains_all?(prop_values, filter_values)
150
- when Operator::SET_CONTAINS_ANY
151
- matches_set_contains_any?(prop_values, filter_values)
152
- when Operator::SET_DOES_NOT_CONTAIN_ANY
153
- !matches_set_contains_any?(prop_values, filter_values)
154
- else
155
- false
141
+ def match_set(prop_values, op, filter_values)
142
+ case op
143
+ when Operator::SET_IS
144
+ set_equals?(prop_values, filter_values)
145
+ when Operator::SET_IS_NOT
146
+ !set_equals?(prop_values, filter_values)
147
+ when Operator::SET_CONTAINS
148
+ matches_set_contains_all?(prop_values, filter_values)
149
+ when Operator::SET_DOES_NOT_CONTAIN
150
+ !matches_set_contains_all?(prop_values, filter_values)
151
+ when Operator::SET_CONTAINS_ANY
152
+ matches_set_contains_any?(prop_values, filter_values)
153
+ when Operator::SET_DOES_NOT_CONTAIN_ANY
154
+ !matches_set_contains_any?(prop_values, filter_values)
155
+ else
156
+ false
157
+ end
156
158
  end
157
- end
158
159
 
159
- def match_string(prop_value, op, filter_values)
160
- case op
161
- when Operator::IS
162
- matches_is?(prop_value, filter_values)
163
- when Operator::IS_NOT
164
- !matches_is?(prop_value, filter_values)
165
- when Operator::CONTAINS
166
- matches_contains?(prop_value, filter_values)
167
- when Operator::DOES_NOT_CONTAIN
168
- !matches_contains?(prop_value, filter_values)
169
- when Operator::LESS_THAN, Operator::LESS_THAN_EQUALS,
170
- Operator::GREATER_THAN, Operator::GREATER_THAN_EQUALS
171
- matches_comparable?(prop_value, op, filter_values,
172
- method(:parse_number),
173
- method(:comparator))
174
- when Operator::VERSION_LESS_THAN, Operator::VERSION_LESS_THAN_EQUALS,
175
- Operator::VERSION_GREATER_THAN, Operator::VERSION_GREATER_THAN_EQUALS
176
- matches_comparable?(prop_value, op, filter_values,
177
- SemanticVersion.method(:parse),
178
- method(:comparator))
179
- when Operator::REGEX_MATCH
180
- matches_regex?(prop_value, filter_values)
181
- when Operator::REGEX_DOES_NOT_MATCH
182
- !matches_regex?(prop_value, filter_values)
183
- else
184
- false
160
+ def match_string(prop_value, op, filter_values)
161
+ case op
162
+ when Operator::IS
163
+ matches_is?(prop_value, filter_values)
164
+ when Operator::IS_NOT
165
+ !matches_is?(prop_value, filter_values)
166
+ when Operator::CONTAINS
167
+ matches_contains?(prop_value, filter_values)
168
+ when Operator::DOES_NOT_CONTAIN
169
+ !matches_contains?(prop_value, filter_values)
170
+ when Operator::LESS_THAN, Operator::LESS_THAN_EQUALS,
171
+ Operator::GREATER_THAN, Operator::GREATER_THAN_EQUALS
172
+ matches_comparable?(prop_value, op, filter_values,
173
+ method(:parse_number),
174
+ method(:comparator))
175
+ when Operator::VERSION_LESS_THAN, Operator::VERSION_LESS_THAN_EQUALS,
176
+ Operator::VERSION_GREATER_THAN, Operator::VERSION_GREATER_THAN_EQUALS
177
+ matches_comparable?(prop_value, op, filter_values,
178
+ SemanticVersion.method(:parse),
179
+ method(:comparator))
180
+ when Operator::REGEX_MATCH
181
+ matches_regex?(prop_value, filter_values)
182
+ when Operator::REGEX_DOES_NOT_MATCH
183
+ !matches_regex?(prop_value, filter_values)
184
+ else
185
+ false
186
+ end
185
187
  end
186
- end
187
188
 
188
- def matches_is?(prop_value, filter_values)
189
- if contains_booleans?(filter_values)
190
- lower = prop_value.downcase
191
- return filter_values.any? { |value| value.downcase == lower } if %w[true false].include?(lower)
189
+ def matches_is?(prop_value, filter_values)
190
+ if contains_booleans?(filter_values)
191
+ lower = prop_value.downcase
192
+ return filter_values.any? { |value| value.downcase == lower } if %w[true false].include?(lower)
193
+ end
194
+ filter_values.any? { |value| prop_value == value }
192
195
  end
193
- filter_values.any? { |value| prop_value == value }
194
- end
195
196
 
196
- def matches_contains?(prop_value, filter_values)
197
- filter_values.any? do |filter_value|
198
- prop_value.downcase.include?(filter_value.downcase)
197
+ def matches_contains?(prop_value, filter_values)
198
+ filter_values.any? do |filter_value|
199
+ prop_value.downcase.include?(filter_value.downcase)
200
+ end
199
201
  end
200
- end
201
202
 
202
- def matches_comparable?(prop_value, op, filter_values, type_transformer, type_comparator)
203
- prop_value_transformed = type_transformer.call(prop_value)
204
- filter_values_transformed = filter_values
205
- .map { |filter_value| type_transformer.call(filter_value) }
206
- .compact
207
-
208
- if !prop_value_transformed || filter_values_transformed.empty?
209
- filter_values.any? { |filter_value| comparator(prop_value, op, filter_value) }
210
- else
211
- filter_values_transformed.any? do |filter_value_transformed|
212
- type_comparator.call(prop_value_transformed, op, filter_value_transformed)
203
+ def matches_comparable?(prop_value, op, filter_values, type_transformer, type_comparator)
204
+ prop_value_transformed = type_transformer.call(prop_value)
205
+ filter_values_transformed = filter_values
206
+ .map { |filter_value| type_transformer.call(filter_value) }
207
+ .compact
208
+
209
+ if !prop_value_transformed || filter_values_transformed.empty?
210
+ filter_values.any? { |filter_value| comparator(prop_value, op, filter_value) }
211
+ else
212
+ filter_values_transformed.any? do |filter_value_transformed|
213
+ type_comparator.call(prop_value_transformed, op, filter_value_transformed)
214
+ end
213
215
  end
214
216
  end
215
- end
216
217
 
217
- def comparator(prop_value, op, filter_value)
218
- case op
219
- when Operator::LESS_THAN, Operator::VERSION_LESS_THAN
220
- prop_value < filter_value
221
- when Operator::LESS_THAN_EQUALS, Operator::VERSION_LESS_THAN_EQUALS
222
- prop_value <= filter_value
223
- when Operator::GREATER_THAN, Operator::VERSION_GREATER_THAN
224
- prop_value > filter_value
225
- when Operator::GREATER_THAN_EQUALS, Operator::VERSION_GREATER_THAN_EQUALS
226
- prop_value >= filter_value
227
- else
228
- false
218
+ def comparator(prop_value, op, filter_value)
219
+ case op
220
+ when Operator::LESS_THAN, Operator::VERSION_LESS_THAN
221
+ prop_value < filter_value
222
+ when Operator::LESS_THAN_EQUALS, Operator::VERSION_LESS_THAN_EQUALS
223
+ prop_value <= filter_value
224
+ when Operator::GREATER_THAN, Operator::VERSION_GREATER_THAN
225
+ prop_value > filter_value
226
+ when Operator::GREATER_THAN_EQUALS, Operator::VERSION_GREATER_THAN_EQUALS
227
+ prop_value >= filter_value
228
+ else
229
+ false
230
+ end
229
231
  end
230
- end
231
232
 
232
- def matches_regex?(prop_value, filter_values)
233
- filter_values.any? { |filter_value| !!(Regexp.new(filter_value) =~ prop_value) }
234
- end
233
+ def matches_regex?(prop_value, filter_values)
234
+ filter_values.any? { |filter_value| !!(Regexp.new(filter_value) =~ prop_value) }
235
+ end
235
236
 
236
- def contains_none?(filter_values)
237
- filter_values.any? { |filter_value| filter_value == '(none)' }
238
- end
237
+ def contains_none?(filter_values)
238
+ filter_values.any? { |filter_value| filter_value == '(none)' }
239
+ end
239
240
 
240
- def contains_booleans?(filter_values)
241
- filter_values.any? do |filter_value|
242
- case filter_value.downcase
243
- when 'true', 'false'
244
- true
245
- else
246
- false
241
+ def contains_booleans?(filter_values)
242
+ filter_values.any? do |filter_value|
243
+ case filter_value.downcase
244
+ when 'true', 'false'
245
+ true
246
+ else
247
+ false
248
+ end
247
249
  end
248
250
  end
249
- end
250
251
 
251
- def parse_number(value)
252
- Float(value)
253
- rescue StandardError
254
- nil
255
- end
252
+ def parse_number(value)
253
+ Float(value)
254
+ rescue StandardError
255
+ nil
256
+ end
256
257
 
257
- def coerce_string(value)
258
- return nil if value.nil?
259
- return value.to_json if value.is_a?(Hash)
258
+ def coerce_string(value)
259
+ return nil if value.nil?
260
+ return value.to_json if value.is_a?(Hash)
260
261
 
261
- value.to_s
262
- end
262
+ value.to_s
263
+ end
263
264
 
264
- def coerce_string_array(value)
265
- if value.is_a?(Array)
266
- value.map { |e| coerce_string(e) }.compact
267
- else
268
- string_value = value.to_s
269
- begin
270
- parsed_value = JSON.parse(string_value)
271
- if parsed_value.is_a?(Array)
272
- parsed_value.map { |e| coerce_string(e) }.compact
273
- else
265
+ def coerce_string_array(value)
266
+ if value.is_a?(Array)
267
+ value.map { |e| coerce_string(e) }.compact
268
+ else
269
+ string_value = value.to_s
270
+ begin
271
+ parsed_value = JSON.parse(string_value)
272
+ if parsed_value.is_a?(Array)
273
+ parsed_value.map { |e| coerce_string(e) }.compact
274
+ else
275
+ s = coerce_string(string_value)
276
+ s ? [s] : nil
277
+ end
278
+ rescue JSON::ParserError
274
279
  s = coerce_string(string_value)
275
280
  s ? [s] : nil
276
281
  end
277
- rescue JSON::ParserError
278
- s = coerce_string(string_value)
279
- s ? [s] : nil
280
282
  end
281
283
  end
282
- end
283
284
 
284
- def set_operator?(op)
285
- case op
286
- when Operator::SET_IS, Operator::SET_IS_NOT,
287
- Operator::SET_CONTAINS, Operator::SET_DOES_NOT_CONTAIN,
288
- Operator::SET_CONTAINS_ANY, Operator::SET_DOES_NOT_CONTAIN_ANY
289
- true
290
- else
291
- false
285
+ def set_operator?(op)
286
+ case op
287
+ when Operator::SET_IS, Operator::SET_IS_NOT,
288
+ Operator::SET_CONTAINS, Operator::SET_DOES_NOT_CONTAIN,
289
+ Operator::SET_CONTAINS_ANY, Operator::SET_DOES_NOT_CONTAIN_ANY
290
+ true
291
+ else
292
+ false
293
+ end
292
294
  end
293
- end
294
295
 
295
- def set_equals?(xa, ya)
296
- xs = Set.new(xa)
297
- ys = Set.new(ya)
298
- xs.size == ys.size && ys.all? { |y| xs.include?(y) }
299
- end
296
+ def set_equals?(xa, ya)
297
+ xs = Set.new(xa)
298
+ ys = Set.new(ya)
299
+ xs.size == ys.size && ys.all? { |y| xs.include?(y) }
300
+ end
300
301
 
301
- def matches_set_contains_all?(prop_values, filter_values)
302
- return false if prop_values.length < filter_values.length
302
+ def matches_set_contains_all?(prop_values, filter_values)
303
+ return false if prop_values.length < filter_values.length
303
304
 
304
- filter_values.all? { |filter_value| matches_is?(filter_value, prop_values) }
305
- end
305
+ filter_values.all? { |filter_value| matches_is?(filter_value, prop_values) }
306
+ end
306
307
 
307
- def matches_set_contains_any?(prop_values, filter_values)
308
- filter_values.any? { |filter_value| matches_is?(filter_value, prop_values) }
308
+ def matches_set_contains_any?(prop_values, filter_values)
309
+ filter_values.any? { |filter_value| matches_is?(filter_value, prop_values) }
310
+ end
309
311
  end
310
312
  end
311
313
  end
@@ -2,122 +2,124 @@
2
2
 
3
3
  require 'json'
4
4
 
5
- module Evaluation
6
- class Distribution
7
- attr_accessor :variant, :range
8
-
9
- def self.from_hash(hash)
10
- new.tap do |dist|
11
- dist.variant = hash['variant']
12
- dist.range = hash['range']
5
+ module AmplitudeExperiment
6
+ module Evaluation
7
+ class Distribution
8
+ attr_accessor :variant, :range
9
+
10
+ def self.from_hash(hash)
11
+ new.tap do |dist|
12
+ dist.variant = hash['variant']
13
+ dist.range = hash['range']
14
+ end
13
15
  end
14
16
  end
15
- end
16
17
 
17
- class Allocation
18
- attr_accessor :range, :distributions
18
+ class Allocation
19
+ attr_accessor :range, :distributions
19
20
 
20
- def self.from_hash(hash)
21
- new.tap do |alloc|
22
- alloc.range = hash['range']
23
- alloc.distributions = hash['distributions']&.map { |d| Distribution.from_hash(d) }
21
+ def self.from_hash(hash)
22
+ new.tap do |alloc|
23
+ alloc.range = hash['range']
24
+ alloc.distributions = hash['distributions']&.map { |d| Distribution.from_hash(d) }
25
+ end
24
26
  end
25
27
  end
26
- end
27
28
 
28
- class Condition
29
- attr_accessor :selector, :op, :values
29
+ class Condition
30
+ attr_accessor :selector, :op, :values
30
31
 
31
- def self.from_hash(hash)
32
- new.tap do |cond|
33
- cond.selector = hash['selector']
34
- cond.op = hash['op']
35
- cond.values = hash['values']
32
+ def self.from_hash(hash)
33
+ new.tap do |cond|
34
+ cond.selector = hash['selector']
35
+ cond.op = hash['op']
36
+ cond.values = hash['values']
37
+ end
36
38
  end
37
39
  end
38
- end
39
40
 
40
- class Bucket
41
- attr_accessor :selector, :salt, :allocations
41
+ class Bucket
42
+ attr_accessor :selector, :salt, :allocations
42
43
 
43
- def self.from_hash(hash)
44
- new.tap do |bucket|
45
- bucket.selector = hash['selector']
46
- bucket.salt = hash['salt']
47
- bucket.allocations = hash['allocations']&.map { |a| Allocation.from_hash(a) }
44
+ def self.from_hash(hash)
45
+ new.tap do |bucket|
46
+ bucket.selector = hash['selector']
47
+ bucket.salt = hash['salt']
48
+ bucket.allocations = hash['allocations']&.map { |a| Allocation.from_hash(a) }
49
+ end
48
50
  end
49
51
  end
50
- end
51
52
 
52
- class Segment
53
- attr_accessor :bucket, :conditions, :variant, :metadata
53
+ class Segment
54
+ attr_accessor :bucket, :conditions, :variant, :metadata
54
55
 
55
- def self.from_hash(hash)
56
- new.tap do |segment|
57
- segment.bucket = hash['bucket'] && Bucket.from_hash(hash['bucket'])
58
- segment.conditions = hash['conditions']&.map { |c| c.map { |inner| Condition.from_hash(inner) } }
59
- segment.variant = hash['variant']
60
- segment.metadata = hash['metadata']
56
+ def self.from_hash(hash)
57
+ new.tap do |segment|
58
+ segment.bucket = hash['bucket'] && Bucket.from_hash(hash['bucket'])
59
+ segment.conditions = hash['conditions']&.map { |c| c.map { |inner| Condition.from_hash(inner) } }
60
+ segment.variant = hash['variant']
61
+ segment.metadata = hash['metadata']
62
+ end
61
63
  end
62
64
  end
63
- end
64
65
 
65
- class Variant
66
- attr_accessor :key, :value, :payload, :metadata
66
+ class Variant
67
+ attr_accessor :key, :value, :payload, :metadata
67
68
 
68
- def [](key)
69
- instance_variable_get("@#{key}")
70
- end
69
+ def [](key)
70
+ instance_variable_get("@#{key}")
71
+ end
71
72
 
72
- def self.from_hash(hash)
73
- new.tap do |variant|
74
- variant.key = hash['key']
75
- variant.value = hash['value']
76
- variant.payload = hash['payload']
77
- variant.metadata = hash['metadata']
73
+ def self.from_hash(hash)
74
+ new.tap do |variant|
75
+ variant.key = hash['key']
76
+ variant.value = hash['value']
77
+ variant.payload = hash['payload']
78
+ variant.metadata = hash['metadata']
79
+ end
78
80
  end
79
81
  end
80
- end
81
82
 
82
- class Flag
83
- attr_accessor :key, :variants, :segments, :dependencies, :metadata
83
+ class Flag
84
+ attr_accessor :key, :variants, :segments, :dependencies, :metadata
85
+
86
+ def self.from_hash(hash)
87
+ new.tap do |flag|
88
+ flag.key = hash['key']
89
+ flag.variants = hash['variants'].transform_values { |v| Variant.from_hash(v) }
90
+ flag.segments = hash['segments'].map { |s| Segment.from_hash(s) }
91
+ flag.dependencies = hash['dependencies']
92
+ flag.metadata = hash['metadata']
93
+ end
94
+ end
84
95
 
85
- def self.from_hash(hash)
86
- new.tap do |flag|
87
- flag.key = hash['key']
88
- flag.variants = hash['variants'].transform_values { |v| Variant.from_hash(v) }
89
- flag.segments = hash['segments'].map { |s| Segment.from_hash(s) }
90
- flag.dependencies = hash['dependencies']
91
- flag.metadata = hash['metadata']
96
+ # Used for testing
97
+ def ==(other)
98
+ key == other.key
92
99
  end
93
100
  end
94
101
 
95
- # Used for testing
96
- def ==(other)
97
- key == other.key
102
+ module Operator
103
+ IS = 'is'
104
+ IS_NOT = 'is not'
105
+ CONTAINS = 'contains'
106
+ DOES_NOT_CONTAIN = 'does not contain'
107
+ LESS_THAN = 'less'
108
+ LESS_THAN_EQUALS = 'less or equal'
109
+ GREATER_THAN = 'greater'
110
+ GREATER_THAN_EQUALS = 'greater or equal'
111
+ VERSION_LESS_THAN = 'version less'
112
+ VERSION_LESS_THAN_EQUALS = 'version less or equal'
113
+ VERSION_GREATER_THAN = 'version greater'
114
+ VERSION_GREATER_THAN_EQUALS = 'version greater or equal'
115
+ SET_IS = 'set is'
116
+ SET_IS_NOT = 'set is not'
117
+ SET_CONTAINS = 'set contains'
118
+ SET_DOES_NOT_CONTAIN = 'set does not contain'
119
+ SET_CONTAINS_ANY = 'set contains any'
120
+ SET_DOES_NOT_CONTAIN_ANY = 'set does not contain any'
121
+ REGEX_MATCH = 'regex match'
122
+ REGEX_DOES_NOT_MATCH = 'regex does not match'
98
123
  end
99
124
  end
100
-
101
- module Operator
102
- IS = 'is'
103
- IS_NOT = 'is not'
104
- CONTAINS = 'contains'
105
- DOES_NOT_CONTAIN = 'does not contain'
106
- LESS_THAN = 'less'
107
- LESS_THAN_EQUALS = 'less or equal'
108
- GREATER_THAN = 'greater'
109
- GREATER_THAN_EQUALS = 'greater or equal'
110
- VERSION_LESS_THAN = 'version less'
111
- VERSION_LESS_THAN_EQUALS = 'version less or equal'
112
- VERSION_GREATER_THAN = 'version greater'
113
- VERSION_GREATER_THAN_EQUALS = 'version greater or equal'
114
- SET_IS = 'set is'
115
- SET_IS_NOT = 'set is not'
116
- SET_CONTAINS = 'set contains'
117
- SET_DOES_NOT_CONTAIN = 'set does not contain'
118
- SET_CONTAINS_ANY = 'set contains any'
119
- SET_DOES_NOT_CONTAIN_ANY = 'set does not contain any'
120
- REGEX_MATCH = 'regex match'
121
- REGEX_DOES_NOT_MATCH = 'regex does not match'
122
- end
123
125
  end
@@ -1,104 +1,108 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Implements 32-bit x86 MurmurHash3
4
- class Murmur3
5
- C1_32 = -0x3361d2af
6
- C2_32 = 0x1b873593
7
- R1_32 = 15
8
- R2_32 = 13
9
- M_32 = 5
10
- N_32 = -0x19ab949c
4
+ module AmplitudeExperiment
5
+ module Evaluation
6
+ class Murmur3
7
+ C1_32 = -0x3361d2af
8
+ C2_32 = 0x1b873593
9
+ R1_32 = 15
10
+ R2_32 = 13
11
+ M_32 = 5
12
+ N_32 = -0x19ab949c
11
13
 
12
- class << self
13
- def hash32x86(input, seed = 0)
14
- data = string_to_utf8_bytes(input)
15
- length = data.length
16
- n_blocks = length >> 2
17
- hash = seed
14
+ class << self
15
+ def hash32x86(input, seed = 0)
16
+ data = string_to_utf8_bytes(input)
17
+ length = data.length
18
+ n_blocks = length >> 2
19
+ hash = seed
18
20
 
19
- # Process body
20
- n_blocks.times do |i|
21
- index = i << 2
22
- k = read_int_le(data, index)
23
- hash = mix32(k, hash)
24
- end
21
+ # Process body
22
+ n_blocks.times do |i|
23
+ index = i << 2
24
+ k = read_int_le(data, index)
25
+ hash = mix32(k, hash)
26
+ end
25
27
 
26
- # Process tail
27
- index = n_blocks << 2
28
- k1 = 0
28
+ # Process tail
29
+ index = n_blocks << 2
30
+ k1 = 0
29
31
 
30
- case length - index
31
- when 3
32
- k1 ^= data[index + 2] << 16
33
- k1 ^= data[index + 1] << 8
34
- k1 ^= data[index]
35
- k1 = (k1 * C1_32) & 0xffffffff
36
- k1 = rotate_left(k1, R1_32)
37
- k1 = (k1 * C2_32) & 0xffffffff
38
- hash ^= k1
39
- when 2
40
- k1 ^= data[index + 1] << 8
41
- k1 ^= data[index]
42
- k1 = (k1 * C1_32) & 0xffffffff
43
- k1 = rotate_left(k1, R1_32)
44
- k1 = (k1 * C2_32) & 0xffffffff
45
- hash ^= k1
46
- when 1
47
- k1 ^= data[index]
48
- k1 = (k1 * C1_32) & 0xffffffff
49
- k1 = rotate_left(k1, R1_32)
50
- k1 = (k1 * C2_32) & 0xffffffff
51
- hash ^= k1
52
- end
32
+ case length - index
33
+ when 3
34
+ k1 ^= data[index + 2] << 16
35
+ k1 ^= data[index + 1] << 8
36
+ k1 ^= data[index]
37
+ k1 = (k1 * C1_32) & 0xffffffff
38
+ k1 = rotate_left(k1, R1_32)
39
+ k1 = (k1 * C2_32) & 0xffffffff
40
+ hash ^= k1
41
+ when 2
42
+ k1 ^= data[index + 1] << 8
43
+ k1 ^= data[index]
44
+ k1 = (k1 * C1_32) & 0xffffffff
45
+ k1 = rotate_left(k1, R1_32)
46
+ k1 = (k1 * C2_32) & 0xffffffff
47
+ hash ^= k1
48
+ when 1
49
+ k1 ^= data[index]
50
+ k1 = (k1 * C1_32) & 0xffffffff
51
+ k1 = rotate_left(k1, R1_32)
52
+ k1 = (k1 * C2_32) & 0xffffffff
53
+ hash ^= k1
54
+ end
53
55
 
54
- hash ^= length
55
- fmix32(hash) & 0xffffffff
56
- end
56
+ hash ^= length
57
+ fmix32(hash) & 0xffffffff
58
+ end
57
59
 
58
- private
60
+ private
59
61
 
60
- def mix32(k, hash)
61
- k = (k * C1_32) & 0xffffffff
62
- k = rotate_left(k, R1_32)
63
- k = (k * C2_32) & 0xffffffff
64
- hash ^= k
65
- hash = rotate_left(hash, R2_32)
66
- ((hash * M_32) + N_32) & 0xffffffff
67
- end
62
+ def mix32(k, hash)
63
+ k = (k * C1_32) & 0xffffffff
64
+ k = rotate_left(k, R1_32)
65
+ k = (k * C2_32) & 0xffffffff
66
+ hash ^= k
67
+ hash = rotate_left(hash, R2_32)
68
+ ((hash * M_32) + N_32) & 0xffffffff
69
+ end
68
70
 
69
- def fmix32(hash)
70
- hash ^= hash >> 16
71
- hash = (hash * -0x7a143595) & 0xffffffff
72
- hash ^= hash >> 13
73
- hash = (hash * -0x3d4d51cb) & 0xffffffff
74
- hash ^= hash >> 16
75
- hash
76
- end
71
+ def fmix32(hash)
72
+ hash ^= hash >> 16
73
+ hash = (hash * -0x7a143595) & 0xffffffff
74
+ hash ^= hash >> 13
75
+ hash = (hash * -0x3d4d51cb) & 0xffffffff
76
+ hash ^= hash >> 16
77
+ hash
78
+ end
77
79
 
78
- def rotate_left(x, n, width = 32)
79
- n = n % width if n > width
80
- mask = (0xffffffff << (width - n)) & 0xffffffff
81
- r = ((x & mask) >> (width - n)) & 0xffffffff
82
- ((x << n) | r) & 0xffffffff
83
- end
80
+ def rotate_left(x, n, width = 32)
81
+ n %= width if n > width
82
+ mask = (0xffffffff << (width - n)) & 0xffffffff
83
+ r = ((x & mask) >> (width - n)) & 0xffffffff
84
+ ((x << n) | r) & 0xffffffff
85
+ end
84
86
 
85
- def read_int_le(data, index = 0)
86
- n = (data[index] << 24) |
87
- (data[index + 1] << 16) |
88
- (data[index + 2] << 8) |
89
- data[index + 3]
90
- reverse_bytes(n)
91
- end
87
+ def read_int_le(data, index = 0)
88
+ n = (data[index] << 24) |
89
+ (data[index + 1] << 16) |
90
+ (data[index + 2] << 8) |
91
+ data[index + 3]
92
+ reverse_bytes(n)
93
+ end
92
94
 
93
- def reverse_bytes(n)
94
- ((n & -0x1000000) >> 24) |
95
- ((n & 0x00ff0000) >> 8) |
96
- ((n & 0x0000ff00) << 8) |
97
- ((n & 0x000000ff) << 24)
98
- end
95
+ def reverse_bytes(n)
96
+ ((n & -0x1000000) >> 24) |
97
+ ((n & 0x00ff0000) >> 8) |
98
+ ((n & 0x0000ff00) << 8) |
99
+ ((n & 0x000000ff) << 24)
100
+ end
99
101
 
100
- def string_to_utf8_bytes(str)
101
- str.encode('UTF-8').bytes
102
+ def string_to_utf8_bytes(str)
103
+ str.encode('UTF-8').bytes
104
+ end
105
+ end
102
106
  end
103
107
  end
104
108
  end
@@ -1,16 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Selects a value from a nested object using an array of selector keys
4
- module Evaluation
5
- def self.select(selectable, selector)
6
- return nil if selector.nil? || selector.empty?
4
+ module AmplitudeExperiment
5
+ module Evaluation
6
+ def self.select(selectable, selector)
7
+ return nil if selector.nil? || selector.empty?
7
8
 
8
- selector.each do |selector_element|
9
- return nil if selector_element.nil? || selectable.nil?
9
+ selector.each do |selector_element|
10
+ return nil if selector_element.nil? || selectable.nil?
10
11
 
11
- selectable = selectable[selector_element]
12
- end
12
+ selectable = selectable[selector_element]
13
+ end
13
14
 
14
- selectable.nil? ? nil : selectable
15
+ selectable.nil? ? nil : selectable
16
+ end
15
17
  end
16
18
  end
@@ -1,52 +1,56 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class SemanticVersion
4
- include Comparable
3
+ module AmplitudeExperiment
4
+ module Evaluation
5
+ class SemanticVersion
6
+ include Comparable
5
7
 
6
- attr_reader :major, :minor, :patch, :pre_release
8
+ attr_reader :major, :minor, :patch, :pre_release
7
9
 
8
- MAJOR_MINOR_REGEX = '(\d+)\.(\d+)'
9
- PATCH_REGEX = '(\d+)'
10
- PRERELEASE_REGEX = '(-(([-\w]+\.?)*))?'
11
- VERSION_PATTERN = /^#{MAJOR_MINOR_REGEX}(\.#{PATCH_REGEX}#{PRERELEASE_REGEX})?$/.freeze
10
+ MAJOR_MINOR_REGEX = '(\d+)\.(\d+)'
11
+ PATCH_REGEX = '(\d+)'
12
+ PRERELEASE_REGEX = '(-(([-\w]+\.?)*))?'
13
+ VERSION_PATTERN = /^#{MAJOR_MINOR_REGEX}(\.#{PATCH_REGEX}#{PRERELEASE_REGEX})?$/.freeze
12
14
 
13
- def initialize(major, minor, patch, pre_release = nil)
14
- @major = major
15
- @minor = minor
16
- @patch = patch
17
- @pre_release = pre_release
18
- end
15
+ def initialize(major, minor, patch, pre_release = nil)
16
+ @major = major
17
+ @minor = minor
18
+ @patch = patch
19
+ @pre_release = pre_release
20
+ end
19
21
 
20
- def self.parse(version)
21
- return nil if version.nil?
22
+ def self.parse(version)
23
+ return nil if version.nil?
22
24
 
23
- match = VERSION_PATTERN.match(version)
24
- return nil unless match
25
+ match = VERSION_PATTERN.match(version)
26
+ return nil unless match
25
27
 
26
- major = match[1].to_i
27
- minor = match[2].to_i
28
- patch = match[4]&.to_i || 0
29
- pre_release = match[5]
28
+ major = match[1].to_i
29
+ minor = match[2].to_i
30
+ patch = match[4]&.to_i || 0
31
+ pre_release = match[5]
30
32
 
31
- new(major, minor, patch, pre_release)
32
- end
33
+ new(major, minor, patch, pre_release)
34
+ end
33
35
 
34
- def <=>(other)
35
- return nil unless other.is_a?(SemanticVersion)
36
+ def <=>(other)
37
+ return nil unless other.is_a?(SemanticVersion)
36
38
 
37
- result = major <=> other.major
38
- return result unless result.zero?
39
+ result = major <=> other.major
40
+ return result unless result.zero?
39
41
 
40
- result = minor <=> other.minor
41
- return result unless result.zero?
42
+ result = minor <=> other.minor
43
+ return result unless result.zero?
42
44
 
43
- result = patch <=> other.patch
44
- return result unless result.zero?
45
+ result = patch <=> other.patch
46
+ return result unless result.zero?
45
47
 
46
- return 1 if !pre_release && other.pre_release
47
- return -1 if pre_release && !other.pre_release
48
- return 0 if !pre_release && !other.pre_release
48
+ return 1 if !pre_release && other.pre_release
49
+ return -1 if pre_release && !other.pre_release
50
+ return 0 if !pre_release && !other.pre_release
49
51
 
50
- pre_release <=> other.pre_release
52
+ pre_release <=> other.pre_release
53
+ end
54
+ end
51
55
  end
52
56
  end
@@ -1,56 +1,53 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class CycleError < StandardError
4
- attr_accessor :path
5
-
6
- def initialize(path)
7
- super("Detected a cycle between flags #{path}")
8
- self.path = path
9
- end
10
- end
3
+ require_relative '../error'
11
4
 
12
5
  # Performs topological sorting of feature flags based on their dependencies
13
- class TopologicalSort
14
- # Sort flags topologically based on their dependencies
15
- def self.sort(flags, flag_keys = nil)
16
- available = flags.clone
17
- result = []
18
- starting_keys = flag_keys.nil? || flag_keys.empty? ? flags.keys : flag_keys
19
-
20
- starting_keys.each do |flag_key|
21
- traversal = parent_traversal(flag_key, available)
22
- result.concat(traversal) if traversal
23
- end
24
-
25
- result
26
- end
27
-
28
- # Perform depth-first traversal of flag dependencies
29
- def self.parent_traversal(flag_key, available, path = [])
30
- flag = available[flag_key]
31
- return nil unless flag
32
-
33
- # No dependencies - return flag and remove from available
34
- if !flag.dependencies || flag.dependencies.empty?
35
- available.delete(flag.key)
36
- return [flag]
6
+ module AmplitudeExperiment
7
+ module Evaluation
8
+ class TopologicalSort
9
+ # Sort flags topologically based on their dependencies
10
+ def self.sort(flags, flag_keys = nil)
11
+ available = flags.clone
12
+ result = []
13
+ starting_keys = flag_keys.nil? || flag_keys.empty? ? flags.keys : flag_keys
14
+
15
+ starting_keys.each do |flag_key|
16
+ traversal = parent_traversal(flag_key, available)
17
+ result.concat(traversal) if traversal
18
+ end
19
+
20
+ result
21
+ end
22
+
23
+ # Perform depth-first traversal of flag dependencies
24
+ def self.parent_traversal(flag_key, available, path = [])
25
+ flag = available[flag_key]
26
+ return nil unless flag
27
+
28
+ # No dependencies - return flag and remove from available
29
+ if !flag.dependencies || flag.dependencies.empty?
30
+ available.delete(flag.key)
31
+ return [flag]
32
+ end
33
+
34
+ # Check for cycles
35
+ path.push(flag.key)
36
+ result = []
37
+
38
+ flag.dependencies.each do |parent_key|
39
+ raise CycleError, path if path.any? { |p| p == parent_key }
40
+
41
+ traversal = parent_traversal(parent_key, available, path)
42
+ result.concat(traversal) if traversal
43
+ end
44
+
45
+ result.push(flag)
46
+ path.pop
47
+ available.delete(flag.key)
48
+
49
+ result
50
+ end
37
51
  end
38
-
39
- # Check for cycles
40
- path.push(flag.key)
41
- result = []
42
-
43
- flag.dependencies.each do |parent_key|
44
- raise CycleError, path if path.any? { |p| p == parent_key }
45
-
46
- traversal = parent_traversal(parent_key, available, path)
47
- result.concat(traversal) if traversal
48
- end
49
-
50
- result.push(flag)
51
- path.pop
52
- available.delete(flag.key)
53
-
54
- result
55
52
  end
56
53
  end
@@ -0,0 +1,6 @@
1
+ require 'experiment/evaluation/evaluation'
2
+ require 'experiment/evaluation/flag'
3
+ require 'experiment/evaluation/murmur3'
4
+ require 'experiment/evaluation/select'
5
+ require 'experiment/evaluation/semantic_version'
6
+ require 'experiment/evaluation/topological_sort'
@@ -1,6 +1,8 @@
1
1
  require 'uri'
2
2
  require 'logger'
3
+
3
4
  require_relative '../../amplitude'
5
+ require_relative '../evaluation'
4
6
 
5
7
  module AmplitudeExperiment
6
8
  FLAG_TYPE_MUTUAL_EXCLUSION_GROUP = 'mutual-exclusion-group'.freeze
@@ -63,7 +65,7 @@ module AmplitudeExperiment
63
65
  flags = @flag_config_storage.flag_configs
64
66
  return {} if flags.nil?
65
67
 
66
- sorted_flags = TopologicalSort.sort(flags, flag_keys)
68
+ sorted_flags = Evaluation::TopologicalSort.sort(flags, flag_keys)
67
69
  required_cohorts_in_storage(sorted_flags)
68
70
  user = enrich_user_with_cohorts(user, flags) if @config.cohort_sync_config
69
71
  context = AmplitudeExperiment.user_to_evaluation_context(user)
@@ -1,3 +1,3 @@
1
1
  module AmplitudeExperiment
2
- VERSION = '1.7.0'.freeze
2
+ VERSION = '1.7.1'.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.7.0
4
+ version: 1.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Amplitude
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-11-03 00:00:00.000000000 Z
11
+ date: 2025-12-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 1.2.2
19
+ version: '1.2'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 1.2.2
26
+ version: '1.2'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: psych
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -197,6 +197,7 @@ files:
197
197
  - lib/experiment/cookie.rb
198
198
  - lib/experiment/deployment/deployment_runner.rb
199
199
  - lib/experiment/error.rb
200
+ - lib/experiment/evaluation.rb
200
201
  - lib/experiment/evaluation/evaluation.rb
201
202
  - lib/experiment/evaluation/flag.rb
202
203
  - lib/experiment/evaluation/murmur3.rb