growthbook 1.0.0 → 1.1.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 +4 -4
- data/lib/growthbook/conditions.rb +2 -1
- data/lib/growthbook/context.rb +72 -26
- data/lib/growthbook/decryption_util.rb +35 -0
- data/lib/growthbook/feature_repository.rb +87 -0
- data/lib/growthbook/feature_rule.rb +3 -1
- data/lib/growthbook/fnv.rb +23 -0
- data/lib/growthbook/inline_experiment.rb +1 -1
- data/lib/growthbook/tracking_callback.rb +8 -0
- data/lib/growthbook/util.rb +19 -13
- data/lib/growthbook.rb +4 -0
- metadata +24 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f5919b4bd1e6564233d0fb61f00f2fcc5fee175f3122a35e9815df47ebdaeb6f
|
|
4
|
+
data.tar.gz: 88bf0372e161e261a536fd8953f858e9f4ad60592a96cbfb8e3baa523414cbca
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 771a1f4fd4eeb6acb92606e67677d65717e3b3a98de80528d2adef3cce17f183a6524af1da74cb09ff18d195a062dd1c882c208cb8dfeaf631603c54b7d128e9
|
|
7
|
+
data.tar.gz: 3b2386b97c64ea3609ad907005d90baec0a25e7b51d45bbd973490d66a204255d5a31d1d435da406cfcd8f3443a28bb8163e6f649af114999b847cbe822f2593
|
|
@@ -29,7 +29,6 @@ module Growthbook
|
|
|
29
29
|
when Hash
|
|
30
30
|
return condition.to_h { |k, v| [k.to_s, parse_condition(v)] }
|
|
31
31
|
end
|
|
32
|
-
|
|
33
32
|
condition
|
|
34
33
|
end
|
|
35
34
|
|
|
@@ -68,6 +67,8 @@ module Growthbook
|
|
|
68
67
|
end
|
|
69
68
|
|
|
70
69
|
def self.get_path(attributes, path)
|
|
70
|
+
path = path.to_s if path.is_a?(Symbol)
|
|
71
|
+
|
|
71
72
|
parts = path.split('.')
|
|
72
73
|
current = attributes
|
|
73
74
|
|
data/lib/growthbook/context.rb
CHANGED
|
@@ -12,7 +12,7 @@ module Growthbook
|
|
|
12
12
|
# @return [true, false, nil] If true, random assignment is disabled and only explicitly forced variations are used.
|
|
13
13
|
attr_accessor :qa_mode
|
|
14
14
|
|
|
15
|
-
# @return [
|
|
15
|
+
# @return [Growthbook::TrackingCallback] An object that responds to `on_experiment_viewed(GrowthBook::InlineExperiment, GrowthBook::InlineExperimentResult)`
|
|
16
16
|
attr_accessor :listener
|
|
17
17
|
|
|
18
18
|
# @return [Hash] Map of user attributes that are used to assign variations
|
|
@@ -24,7 +24,11 @@ module Growthbook
|
|
|
24
24
|
# @return [Hash] Force specific experiments to always assign a specific variation (used for QA)
|
|
25
25
|
attr_reader :forced_variations
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
# @return [Hash[String, Growthbook::InlineExperimentResult]] Tracked impressions
|
|
28
|
+
attr_reader :impressions
|
|
29
|
+
|
|
30
|
+
# @return [Hash[String, Any]] Forced feature values
|
|
31
|
+
attr_reader :forced_features
|
|
28
32
|
|
|
29
33
|
def initialize(options = {})
|
|
30
34
|
@features = {}
|
|
@@ -34,14 +38,19 @@ module Growthbook
|
|
|
34
38
|
@enabled = true
|
|
35
39
|
@impressions = {}
|
|
36
40
|
|
|
37
|
-
options.each do |key, value|
|
|
38
|
-
case key
|
|
41
|
+
options.transform_keys(&:to_sym).each do |key, value|
|
|
42
|
+
case key
|
|
39
43
|
when :enabled
|
|
40
44
|
@enabled = value
|
|
41
45
|
when :attributes
|
|
42
46
|
self.attributes = value
|
|
43
47
|
when :url
|
|
44
48
|
@url = value
|
|
49
|
+
when :decryption_key
|
|
50
|
+
nil
|
|
51
|
+
when :encrypted_features
|
|
52
|
+
decrypted = decrypted_features_from_options(options)
|
|
53
|
+
self.features = decrypted unless decrypted.nil?
|
|
45
54
|
when :features
|
|
46
55
|
self.features = value
|
|
47
56
|
when :forced_variations, :forcedVariations
|
|
@@ -61,6 +70,8 @@ module Growthbook
|
|
|
61
70
|
def features=(features)
|
|
62
71
|
@features = {}
|
|
63
72
|
|
|
73
|
+
return if features.nil?
|
|
74
|
+
|
|
64
75
|
features.each do |k, v|
|
|
65
76
|
# Convert to a Feature object if it's not already
|
|
66
77
|
v = Growthbook::Feature.new(v) unless v.is_a? Growthbook::Feature
|
|
@@ -83,35 +94,40 @@ module Growthbook
|
|
|
83
94
|
|
|
84
95
|
def eval_feature(key)
|
|
85
96
|
# Forced in the context
|
|
86
|
-
return get_feature_result(@forced_features[key.to_s], 'override') if @forced_features.key?(key.to_s)
|
|
97
|
+
return get_feature_result(@forced_features[key.to_s], 'override', nil, nil) if @forced_features.key?(key.to_s)
|
|
87
98
|
|
|
88
99
|
# Return if we can't find the feature definition
|
|
89
100
|
feature = get_feature(key)
|
|
90
|
-
return get_feature_result(nil, 'unknownFeature') unless feature
|
|
101
|
+
return get_feature_result(nil, 'unknownFeature', nil, nil) unless feature
|
|
91
102
|
|
|
92
103
|
feature.rules.each do |rule|
|
|
93
104
|
# Targeting condition
|
|
94
|
-
next if rule.condition && !condition_passes(rule.condition)
|
|
105
|
+
next if rule.condition && !condition_passes?(rule.condition)
|
|
95
106
|
|
|
96
107
|
# If there are filters for who is included (e.g. namespaces)
|
|
97
|
-
next if rule.filters && filtered_out?(rule.filters)
|
|
108
|
+
next if rule.filters && filtered_out?(rule.filters || [])
|
|
98
109
|
|
|
99
110
|
# If this is a percentage rollout, skip if not included
|
|
100
111
|
if rule.force?
|
|
101
112
|
seed = rule.seed || key
|
|
102
113
|
hash_attribute = rule.hash_attribute || 'id'
|
|
103
114
|
included_in_rollout = included_in_rollout?(
|
|
104
|
-
seed: seed
|
|
105
|
-
|
|
115
|
+
seed: seed.to_s,
|
|
116
|
+
hash_attribute: hash_attribute,
|
|
117
|
+
range: rule.range,
|
|
118
|
+
coverage: rule.coverage,
|
|
119
|
+
hash_version: rule.hash_version
|
|
106
120
|
)
|
|
107
121
|
next unless included_in_rollout
|
|
108
122
|
|
|
109
|
-
return get_feature_result(rule.force, 'force')
|
|
123
|
+
return get_feature_result(rule.force, 'force', nil, nil)
|
|
110
124
|
end
|
|
111
125
|
# Experiment rule
|
|
112
126
|
next unless rule.experiment?
|
|
113
127
|
|
|
114
128
|
exp = rule.to_experiment(key)
|
|
129
|
+
next if exp.nil?
|
|
130
|
+
|
|
115
131
|
result = _run(exp, key)
|
|
116
132
|
|
|
117
133
|
next unless result.in_experiment && !result.passthrough
|
|
@@ -120,7 +136,7 @@ module Growthbook
|
|
|
120
136
|
end
|
|
121
137
|
|
|
122
138
|
# Fallback
|
|
123
|
-
get_feature_result(feature.default_value
|
|
139
|
+
get_feature_result(feature.default_value.nil? ? nil : feature.default_value, 'defaultValue', nil, nil)
|
|
124
140
|
end
|
|
125
141
|
|
|
126
142
|
def run(exp)
|
|
@@ -152,8 +168,9 @@ module Growthbook
|
|
|
152
168
|
return get_experiment_result(exp, -1, hash_used: false, feature_id: feature_id) unless @enabled
|
|
153
169
|
|
|
154
170
|
# 3. If forced via URL querystring
|
|
155
|
-
|
|
156
|
-
|
|
171
|
+
override_url = @url
|
|
172
|
+
unless override_url.nil?
|
|
173
|
+
qs_override = Util.get_query_string_override(key, override_url, exp.variations.length)
|
|
157
174
|
return get_experiment_result(exp, qs_override, hash_used: false, feature_id: feature_id) unless qs_override.nil?
|
|
158
175
|
end
|
|
159
176
|
|
|
@@ -177,13 +194,13 @@ module Growthbook
|
|
|
177
194
|
|
|
178
195
|
# 7. Exclude if user is filtered out (used to be called "namespace")
|
|
179
196
|
if exp.filters
|
|
180
|
-
return get_experiment_result(exp, -1, hash_used: false, feature_id: feature_id) if filtered_out?(exp.filters)
|
|
181
|
-
elsif exp.namespace && !Growthbook::Util.in_namespace(hash_value, exp.namespace)
|
|
197
|
+
return get_experiment_result(exp, -1, hash_used: false, feature_id: feature_id) if filtered_out?(exp.filters || [])
|
|
198
|
+
elsif exp.namespace && !Growthbook::Util.in_namespace?(hash_value, exp.namespace)
|
|
182
199
|
return get_experiment_result(exp, -1, hash_used: false, feature_id: feature_id)
|
|
183
200
|
end
|
|
184
201
|
|
|
185
202
|
# 8. Exclude if condition is false
|
|
186
|
-
if exp.condition && !condition_passes(exp.condition)
|
|
203
|
+
if exp.condition && !condition_passes?(exp.condition)
|
|
187
204
|
return get_experiment_result(
|
|
188
205
|
exp,
|
|
189
206
|
-1,
|
|
@@ -198,7 +215,10 @@ module Growthbook
|
|
|
198
215
|
exp.coverage,
|
|
199
216
|
exp.weights
|
|
200
217
|
)
|
|
201
|
-
|
|
218
|
+
seed = exp.seed || key || ''
|
|
219
|
+
n = Growthbook::Util.get_hash(seed: seed, value: hash_value, version: exp.hash_version || 1)
|
|
220
|
+
return get_experiment_result(exp, -1, hash_used: false, feature_id: feature_id) if n.nil?
|
|
221
|
+
|
|
202
222
|
assigned = Growthbook::Util.choose_variation(n, ranges)
|
|
203
223
|
|
|
204
224
|
# 10. Return if not in experiment
|
|
@@ -228,7 +248,9 @@ module Growthbook
|
|
|
228
248
|
new_hash
|
|
229
249
|
end
|
|
230
250
|
|
|
231
|
-
def condition_passes(condition)
|
|
251
|
+
def condition_passes?(condition)
|
|
252
|
+
return false if condition.nil?
|
|
253
|
+
|
|
232
254
|
Growthbook::Conditions.eval_condition(@attributes, condition)
|
|
233
255
|
end
|
|
234
256
|
|
|
@@ -244,9 +266,18 @@ module Growthbook
|
|
|
244
266
|
meta = experiment.meta ? experiment.meta[variation_index] : {}
|
|
245
267
|
|
|
246
268
|
result = Growthbook::InlineExperimentResult.new(
|
|
247
|
-
{
|
|
248
|
-
|
|
249
|
-
|
|
269
|
+
{
|
|
270
|
+
key: meta['key'] || variation_index,
|
|
271
|
+
in_experiment: in_experiment,
|
|
272
|
+
variation_id: variation_index,
|
|
273
|
+
value: experiment.variations[variation_index],
|
|
274
|
+
hash_used: hash_used,
|
|
275
|
+
hash_attribute: hash_attribute,
|
|
276
|
+
hash_value: hash_value,
|
|
277
|
+
feature_id: feature_id,
|
|
278
|
+
bucket: bucket,
|
|
279
|
+
name: meta['name']
|
|
280
|
+
}
|
|
250
281
|
)
|
|
251
282
|
|
|
252
283
|
result.passthrough = true if meta['passthrough']
|
|
@@ -254,7 +285,7 @@ module Growthbook
|
|
|
254
285
|
result
|
|
255
286
|
end
|
|
256
287
|
|
|
257
|
-
def get_feature_result(value, source, experiment
|
|
288
|
+
def get_feature_result(value, source, experiment, experiment_result)
|
|
258
289
|
Growthbook::FeatureResult.new(value, source, experiment, experiment_result)
|
|
259
290
|
end
|
|
260
291
|
|
|
@@ -273,8 +304,10 @@ module Growthbook
|
|
|
273
304
|
end
|
|
274
305
|
|
|
275
306
|
def track_experiment(experiment, result)
|
|
307
|
+
return if listener.nil?
|
|
308
|
+
|
|
276
309
|
@listener.on_experiment_viewed(experiment, result) if @listener.respond_to?(:on_experiment_viewed)
|
|
277
|
-
@impressions[experiment.key] = result
|
|
310
|
+
@impressions[experiment.key] = result unless experiment.key.nil?
|
|
278
311
|
end
|
|
279
312
|
|
|
280
313
|
def included_in_rollout?(seed:, hash_attribute:, hash_version:, range:, coverage:)
|
|
@@ -284,7 +317,8 @@ module Growthbook
|
|
|
284
317
|
|
|
285
318
|
return false if hash_value.empty?
|
|
286
319
|
|
|
287
|
-
n = Growthbook::Util.
|
|
320
|
+
n = Growthbook::Util.get_hash(seed: seed, value: hash_value, version: hash_version || 1)
|
|
321
|
+
return false if n.nil?
|
|
288
322
|
|
|
289
323
|
return Growthbook::Util.in_range?(n, range) if range
|
|
290
324
|
return n <= coverage if coverage
|
|
@@ -299,11 +333,23 @@ module Growthbook
|
|
|
299
333
|
if hash_value.empty?
|
|
300
334
|
false
|
|
301
335
|
else
|
|
302
|
-
n = Growthbook::Util.
|
|
336
|
+
n = Growthbook::Util.get_hash(seed: filter['seed'] || '', value: hash_value, version: filter['hashVersion'] || 2)
|
|
337
|
+
|
|
338
|
+
return true if n.nil?
|
|
303
339
|
|
|
304
340
|
filter['ranges'].none? { |range| Growthbook::Util.in_range?(n, range) }
|
|
305
341
|
end
|
|
306
342
|
end
|
|
307
343
|
end
|
|
344
|
+
|
|
345
|
+
def decrypted_features_from_options(options)
|
|
346
|
+
decrypted_features = DecryptionUtil.decrypt(options[:encrypted_features], key: options[:decryption_key])
|
|
347
|
+
|
|
348
|
+
return nil if decrypted_features.nil?
|
|
349
|
+
|
|
350
|
+
JSON.parse(decrypted_features)
|
|
351
|
+
rescue StandardError
|
|
352
|
+
nil
|
|
353
|
+
end
|
|
308
354
|
end
|
|
309
355
|
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'base64'
|
|
4
|
+
require 'openssl'
|
|
5
|
+
|
|
6
|
+
module Growthbook
|
|
7
|
+
# Utils for working with encrypted feature payloads.
|
|
8
|
+
class DecryptionUtil
|
|
9
|
+
# @return [String, nil] The decrypted payload, or nil if it fails to decrypt
|
|
10
|
+
def self.decrypt(payload, key:)
|
|
11
|
+
return nil if payload.nil?
|
|
12
|
+
return nil unless payload.include?('.')
|
|
13
|
+
|
|
14
|
+
parts = payload.split('.')
|
|
15
|
+
return nil if parts.length != 2
|
|
16
|
+
|
|
17
|
+
iv = parts[0]
|
|
18
|
+
decoded_iv = Base64.strict_decode64(iv)
|
|
19
|
+
decoded_key = Base64.strict_decode64(key)
|
|
20
|
+
|
|
21
|
+
cipher_text = parts[1]
|
|
22
|
+
decoded_cipher_text = Base64.strict_decode64(cipher_text)
|
|
23
|
+
|
|
24
|
+
cipher = OpenSSL::Cipher.new('aes-128-cbc')
|
|
25
|
+
|
|
26
|
+
cipher.decrypt
|
|
27
|
+
cipher.key = decoded_key
|
|
28
|
+
cipher.iv = decoded_iv
|
|
29
|
+
|
|
30
|
+
cipher.update(decoded_cipher_text) + cipher.final
|
|
31
|
+
rescue StandardError
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'uri'
|
|
5
|
+
|
|
6
|
+
module Growthbook
|
|
7
|
+
# Optional class for fetching features from the GrowthBook API
|
|
8
|
+
class FeatureRepository
|
|
9
|
+
# [String] The SDK endpoint
|
|
10
|
+
attr_reader :endpoint
|
|
11
|
+
|
|
12
|
+
# [String, nil] Optional key for decrypting an encrypted payload
|
|
13
|
+
attr_reader :decryption_key
|
|
14
|
+
|
|
15
|
+
# Parsed features JSON
|
|
16
|
+
attr_reader :features_json
|
|
17
|
+
|
|
18
|
+
def initialize(endpoint:, decryption_key:)
|
|
19
|
+
@endpoint = endpoint
|
|
20
|
+
@decryption_key = decryption_key
|
|
21
|
+
@features_json = {}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def fetch
|
|
25
|
+
uri = URI(endpoint)
|
|
26
|
+
res = Net::HTTP.get_response(uri)
|
|
27
|
+
|
|
28
|
+
@response = res.is_a?(Net::HTTPSuccess) ? res.body : nil
|
|
29
|
+
|
|
30
|
+
return nil if response.nil?
|
|
31
|
+
|
|
32
|
+
if use_decryption?
|
|
33
|
+
parsed_decrypted_response
|
|
34
|
+
else
|
|
35
|
+
parsed_plain_text_response
|
|
36
|
+
end
|
|
37
|
+
rescue StandardError
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def fetch!
|
|
42
|
+
fetch
|
|
43
|
+
|
|
44
|
+
raise FeatureFetchError if response.nil?
|
|
45
|
+
raise FeatureParseError if features_json.nil? || features_json.empty?
|
|
46
|
+
|
|
47
|
+
features_json
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
attr_reader :response
|
|
53
|
+
|
|
54
|
+
def use_decryption?
|
|
55
|
+
!decryption_key.nil?
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def parsed_plain_text_response
|
|
59
|
+
@features_json = parsed_response['features'] unless parsed_response.nil?
|
|
60
|
+
rescue StandardError
|
|
61
|
+
nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def parsed_decrypted_response
|
|
65
|
+
k = decryption_key
|
|
66
|
+
return nil if k.nil?
|
|
67
|
+
|
|
68
|
+
decrypted_str = Growthbook::DecryptionUtil.decrypt(parsed_response['encryptedFeatures'], key: k)
|
|
69
|
+
@features_json = JSON.parse(decrypted_str) unless decrypted_str.nil?
|
|
70
|
+
rescue StandardError
|
|
71
|
+
nil
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def parsed_response
|
|
75
|
+
res = response
|
|
76
|
+
return {} if res.nil?
|
|
77
|
+
|
|
78
|
+
JSON.parse(res)
|
|
79
|
+
rescue StandardError
|
|
80
|
+
{}
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
class FeatureFetchError < StandardError; end
|
|
84
|
+
|
|
85
|
+
class FeatureParseError < StandardError; end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# FNV
|
|
4
|
+
# {https://github.com/jakedouglas/fnv-ruby Source}
|
|
5
|
+
class FNV
|
|
6
|
+
INIT32 = 0x811c9dc5
|
|
7
|
+
INIT64 = 0xcbf29ce484222325
|
|
8
|
+
PRIME32 = 0x01000193
|
|
9
|
+
PRIME64 = 0x100000001b3
|
|
10
|
+
MOD32 = 4_294_967_296
|
|
11
|
+
MOD64 = 18_446_744_073_709_551_616
|
|
12
|
+
|
|
13
|
+
def fnv1a_32(data)
|
|
14
|
+
hash = INIT32
|
|
15
|
+
|
|
16
|
+
data.bytes.each do |byte|
|
|
17
|
+
hash = hash ^ byte
|
|
18
|
+
hash = (hash * PRIME32) % MOD32
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
hash
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -56,7 +56,7 @@ module Growthbook
|
|
|
56
56
|
@variations = get_option(options, :variations, [])
|
|
57
57
|
@weights = get_option(options, :weights)
|
|
58
58
|
@active = get_option(options, :active, true)
|
|
59
|
-
@coverage = get_option(options, :coverage, 1)
|
|
59
|
+
@coverage = get_option(options, :coverage, 1.0)
|
|
60
60
|
@ranges = get_option(options, :ranges)
|
|
61
61
|
@condition = get_option(options, :condition)
|
|
62
62
|
@namespace = get_option(options, :namespace)
|
data/lib/growthbook/util.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require '
|
|
3
|
+
require 'base64'
|
|
4
4
|
require 'bigdecimal'
|
|
5
5
|
require 'bigdecimal/util'
|
|
6
6
|
|
|
@@ -42,15 +42,20 @@ module Growthbook
|
|
|
42
42
|
end
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
# @return [Float, nil] Hash, or nil if the hash version is invalid
|
|
46
|
+
def self.get_hash(seed:, value:, version:)
|
|
46
47
|
return (FNV.new.fnv1a_32(value + seed) % 1000) / 1000.0 if version == 1
|
|
47
48
|
return (FNV.new.fnv1a_32(FNV.new.fnv1a_32(seed + value).to_s) % 10_000) / 10_000.0 if version == 2
|
|
48
49
|
|
|
49
|
-
|
|
50
|
+
nil
|
|
50
51
|
end
|
|
51
52
|
|
|
52
|
-
def self.in_namespace(hash_value, namespace)
|
|
53
|
-
|
|
53
|
+
def self.in_namespace?(hash_value, namespace)
|
|
54
|
+
return false if namespace.nil?
|
|
55
|
+
|
|
56
|
+
n = get_hash(seed: "__#{namespace[0]}", value: hash_value, version: 1)
|
|
57
|
+
return false if n.nil?
|
|
58
|
+
|
|
54
59
|
n >= namespace[1] && n < namespace[2]
|
|
55
60
|
end
|
|
56
61
|
|
|
@@ -65,11 +70,11 @@ module Growthbook
|
|
|
65
70
|
end
|
|
66
71
|
|
|
67
72
|
# Determine bucket ranges for experiment variations
|
|
68
|
-
def self.get_bucket_ranges(num_variations, coverage
|
|
73
|
+
def self.get_bucket_ranges(num_variations, coverage, weights)
|
|
69
74
|
# Make sure coverage is within bounds
|
|
70
|
-
coverage = 1 if coverage.nil?
|
|
71
|
-
coverage = 0 if coverage.negative?
|
|
72
|
-
coverage = 1 if coverage > 1
|
|
75
|
+
coverage = 1.0 if coverage.nil?
|
|
76
|
+
coverage = 0.0 if coverage.negative?
|
|
77
|
+
coverage = 1.0 if coverage > 1
|
|
73
78
|
|
|
74
79
|
# Default to equal weights
|
|
75
80
|
weights = get_equal_weights(num_variations) if !weights || weights.length != num_variations
|
|
@@ -79,7 +84,7 @@ module Growthbook
|
|
|
79
84
|
weights = get_equal_weights(num_variations) if total < 0.99 || total > 1.01
|
|
80
85
|
|
|
81
86
|
# Convert weights to ranges
|
|
82
|
-
cumulative = 0
|
|
87
|
+
cumulative = 0.0
|
|
83
88
|
ranges = []
|
|
84
89
|
weights.each do |w|
|
|
85
90
|
start = cumulative
|
|
@@ -102,13 +107,14 @@ module Growthbook
|
|
|
102
107
|
# e.g. http://localhost?my-test=1 will return `1` for id `my-test`
|
|
103
108
|
def self.get_query_string_override(id, url, num_variations)
|
|
104
109
|
# Skip if url is empty
|
|
105
|
-
return nil if url == ''
|
|
110
|
+
return nil if url == '' || id.nil?
|
|
106
111
|
|
|
107
112
|
# Parse out the query string
|
|
108
113
|
parsed = URI(url)
|
|
109
|
-
|
|
114
|
+
parsed_query = parsed.query
|
|
115
|
+
return nil if parsed_query.nil?
|
|
110
116
|
|
|
111
|
-
qs = URI.decode_www_form(
|
|
117
|
+
qs = URI.decode_www_form(parsed_query)
|
|
112
118
|
|
|
113
119
|
# Look for `id` in the querystring and get the value
|
|
114
120
|
vals = qs.assoc(id)
|
data/lib/growthbook.rb
CHANGED
|
@@ -6,9 +6,13 @@ end
|
|
|
6
6
|
|
|
7
7
|
require 'growthbook/conditions'
|
|
8
8
|
require 'growthbook/context'
|
|
9
|
+
require 'growthbook/decryption_util'
|
|
9
10
|
require 'growthbook/feature'
|
|
11
|
+
require 'growthbook/feature_repository'
|
|
10
12
|
require 'growthbook/feature_result'
|
|
11
13
|
require 'growthbook/feature_rule'
|
|
14
|
+
require 'growthbook/fnv'
|
|
12
15
|
require 'growthbook/inline_experiment'
|
|
13
16
|
require 'growthbook/inline_experiment_result'
|
|
17
|
+
require 'growthbook/tracking_callback'
|
|
14
18
|
require 'growthbook/util'
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: growthbook
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- GrowthBook
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2023-
|
|
11
|
+
date: 2023-06-16 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rspec
|
|
@@ -24,6 +24,20 @@ dependencies:
|
|
|
24
24
|
- - "~>"
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
26
|
version: '3.2'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: rspec-its
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '1.3'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '1.3'
|
|
27
41
|
- !ruby/object:Gem::Dependency
|
|
28
42
|
name: simplecov
|
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -53,19 +67,19 @@ dependencies:
|
|
|
53
67
|
- !ruby/object:Gem::Version
|
|
54
68
|
version: 0.1.0
|
|
55
69
|
- !ruby/object:Gem::Dependency
|
|
56
|
-
name:
|
|
70
|
+
name: webmock
|
|
57
71
|
requirement: !ruby/object:Gem::Requirement
|
|
58
72
|
requirements:
|
|
59
73
|
- - "~>"
|
|
60
74
|
- !ruby/object:Gem::Version
|
|
61
|
-
version:
|
|
62
|
-
type: :
|
|
75
|
+
version: '3.18'
|
|
76
|
+
type: :development
|
|
63
77
|
prerelease: false
|
|
64
78
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
79
|
requirements:
|
|
66
80
|
- - "~>"
|
|
67
81
|
- !ruby/object:Gem::Version
|
|
68
|
-
version:
|
|
82
|
+
version: '3.18'
|
|
69
83
|
description: Official GrowthBook SDK for Ruby
|
|
70
84
|
email: jeremy@growthbook.io
|
|
71
85
|
executables: []
|
|
@@ -75,11 +89,15 @@ files:
|
|
|
75
89
|
- lib/growthbook.rb
|
|
76
90
|
- lib/growthbook/conditions.rb
|
|
77
91
|
- lib/growthbook/context.rb
|
|
92
|
+
- lib/growthbook/decryption_util.rb
|
|
78
93
|
- lib/growthbook/feature.rb
|
|
94
|
+
- lib/growthbook/feature_repository.rb
|
|
79
95
|
- lib/growthbook/feature_result.rb
|
|
80
96
|
- lib/growthbook/feature_rule.rb
|
|
97
|
+
- lib/growthbook/fnv.rb
|
|
81
98
|
- lib/growthbook/inline_experiment.rb
|
|
82
99
|
- lib/growthbook/inline_experiment_result.rb
|
|
100
|
+
- lib/growthbook/tracking_callback.rb
|
|
83
101
|
- lib/growthbook/util.rb
|
|
84
102
|
homepage: https://github.com/growthbook/growthbook-ruby
|
|
85
103
|
licenses:
|