growthbook 1.0.0 → 1.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/growthbook/conditions.rb +2 -1
- data/lib/growthbook/context.rb +90 -28
- 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/feature_usage_callback.rb +8 -0
- 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 +5 -0
- metadata +26 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cba2938ec675b1626d643fb7493223a54d0faf1ffc37c2bacb5a06637c0a75cd
|
4
|
+
data.tar.gz: bce59918f346b53182450ec2fdd92776767954712334bc5b7c0bd48d952c05ce
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0c4ac9f40b23c8719b9cb585d92f089144df5441c787321fd0083ae72ff954f5f8027615cc3d226dcdb7c2bfdc3dc5911ef1abd472904c026a3e24c1dd81230c
|
7
|
+
data.tar.gz: 735bfc1c4b82432a4a9b0664282062f43ab644309933afcddb43c9dc53cd32a278d1cf14221ad9b298a0bbdb0e2a12a8e77c4aa814d8ba3719e7840c62a4d09a
|
@@ -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,9 +12,12 @@ 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
|
+
# @return [Growthbook::FeatureUsageCallback] An object that responds to `on_feature_usage(String, Growthbook::FeatureResult)`
|
19
|
+
attr_accessor :on_feature_usage
|
20
|
+
|
18
21
|
# @return [Hash] Map of user attributes that are used to assign variations
|
19
22
|
attr_reader :attributes
|
20
23
|
|
@@ -24,7 +27,11 @@ module Growthbook
|
|
24
27
|
# @return [Hash] Force specific experiments to always assign a specific variation (used for QA)
|
25
28
|
attr_reader :forced_variations
|
26
29
|
|
27
|
-
|
30
|
+
# @return [Hash[String, Growthbook::InlineExperimentResult]] Tracked impressions
|
31
|
+
attr_reader :impressions
|
32
|
+
|
33
|
+
# @return [Hash[String, Any]] Forced feature values
|
34
|
+
attr_reader :forced_features
|
28
35
|
|
29
36
|
def initialize(options = {})
|
30
37
|
@features = {}
|
@@ -34,14 +41,19 @@ module Growthbook
|
|
34
41
|
@enabled = true
|
35
42
|
@impressions = {}
|
36
43
|
|
37
|
-
options.each do |key, value|
|
38
|
-
case key
|
44
|
+
options.transform_keys(&:to_sym).each do |key, value|
|
45
|
+
case key
|
39
46
|
when :enabled
|
40
47
|
@enabled = value
|
41
48
|
when :attributes
|
42
49
|
self.attributes = value
|
43
50
|
when :url
|
44
51
|
@url = value
|
52
|
+
when :decryption_key
|
53
|
+
nil
|
54
|
+
when :encrypted_features
|
55
|
+
decrypted = decrypted_features_from_options(options)
|
56
|
+
self.features = decrypted unless decrypted.nil?
|
45
57
|
when :features
|
46
58
|
self.features = value
|
47
59
|
when :forced_variations, :forcedVariations
|
@@ -52,6 +64,8 @@ module Growthbook
|
|
52
64
|
@qa_mode = value
|
53
65
|
when :listener
|
54
66
|
@listener = value
|
67
|
+
when :on_feature_usage
|
68
|
+
@on_feature_usage = value
|
55
69
|
else
|
56
70
|
warn("Unknown context option: #{key}")
|
57
71
|
end
|
@@ -61,6 +75,8 @@ module Growthbook
|
|
61
75
|
def features=(features)
|
62
76
|
@features = {}
|
63
77
|
|
78
|
+
return if features.nil?
|
79
|
+
|
64
80
|
features.each do |k, v|
|
65
81
|
# Convert to a Feature object if it's not already
|
66
82
|
v = Growthbook::Feature.new(v) unless v.is_a? Growthbook::Feature
|
@@ -83,44 +99,49 @@ module Growthbook
|
|
83
99
|
|
84
100
|
def eval_feature(key)
|
85
101
|
# Forced in the context
|
86
|
-
return get_feature_result(@forced_features[key.to_s], 'override') if @forced_features.key?(key.to_s)
|
102
|
+
return get_feature_result(key.to_s, @forced_features[key.to_s], 'override', nil, nil) if @forced_features.key?(key.to_s)
|
87
103
|
|
88
104
|
# Return if we can't find the feature definition
|
89
105
|
feature = get_feature(key)
|
90
|
-
return get_feature_result(nil, 'unknownFeature') unless feature
|
106
|
+
return get_feature_result(key.to_s, nil, 'unknownFeature', nil, nil) unless feature
|
91
107
|
|
92
108
|
feature.rules.each do |rule|
|
93
109
|
# Targeting condition
|
94
|
-
next if rule.condition && !condition_passes(rule.condition)
|
110
|
+
next if rule.condition && !condition_passes?(rule.condition)
|
95
111
|
|
96
112
|
# If there are filters for who is included (e.g. namespaces)
|
97
|
-
next if rule.filters && filtered_out?(rule.filters)
|
113
|
+
next if rule.filters && filtered_out?(rule.filters || [])
|
98
114
|
|
99
115
|
# If this is a percentage rollout, skip if not included
|
100
116
|
if rule.force?
|
101
117
|
seed = rule.seed || key
|
102
118
|
hash_attribute = rule.hash_attribute || 'id'
|
103
119
|
included_in_rollout = included_in_rollout?(
|
104
|
-
seed: seed
|
105
|
-
|
120
|
+
seed: seed.to_s,
|
121
|
+
hash_attribute: hash_attribute,
|
122
|
+
range: rule.range,
|
123
|
+
coverage: rule.coverage,
|
124
|
+
hash_version: rule.hash_version
|
106
125
|
)
|
107
126
|
next unless included_in_rollout
|
108
127
|
|
109
|
-
return get_feature_result(rule.force, 'force')
|
128
|
+
return get_feature_result(key.to_s, rule.force, 'force', nil, nil)
|
110
129
|
end
|
111
130
|
# Experiment rule
|
112
131
|
next unless rule.experiment?
|
113
132
|
|
114
133
|
exp = rule.to_experiment(key)
|
134
|
+
next if exp.nil?
|
135
|
+
|
115
136
|
result = _run(exp, key)
|
116
137
|
|
117
138
|
next unless result.in_experiment && !result.passthrough
|
118
139
|
|
119
|
-
return get_feature_result(result.value, 'experiment', exp, result)
|
140
|
+
return get_feature_result(key.to_s, result.value, 'experiment', exp, result)
|
120
141
|
end
|
121
142
|
|
122
143
|
# Fallback
|
123
|
-
get_feature_result(feature.default_value
|
144
|
+
get_feature_result(key.to_s, feature.default_value.nil? ? nil : feature.default_value, 'defaultValue', nil, nil)
|
124
145
|
end
|
125
146
|
|
126
147
|
def run(exp)
|
@@ -152,8 +173,9 @@ module Growthbook
|
|
152
173
|
return get_experiment_result(exp, -1, hash_used: false, feature_id: feature_id) unless @enabled
|
153
174
|
|
154
175
|
# 3. If forced via URL querystring
|
155
|
-
|
156
|
-
|
176
|
+
override_url = @url
|
177
|
+
unless override_url.nil?
|
178
|
+
qs_override = Util.get_query_string_override(key, override_url, exp.variations.length)
|
157
179
|
return get_experiment_result(exp, qs_override, hash_used: false, feature_id: feature_id) unless qs_override.nil?
|
158
180
|
end
|
159
181
|
|
@@ -177,13 +199,13 @@ module Growthbook
|
|
177
199
|
|
178
200
|
# 7. Exclude if user is filtered out (used to be called "namespace")
|
179
201
|
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)
|
202
|
+
return get_experiment_result(exp, -1, hash_used: false, feature_id: feature_id) if filtered_out?(exp.filters || [])
|
203
|
+
elsif exp.namespace && !Growthbook::Util.in_namespace?(hash_value, exp.namespace)
|
182
204
|
return get_experiment_result(exp, -1, hash_used: false, feature_id: feature_id)
|
183
205
|
end
|
184
206
|
|
185
207
|
# 8. Exclude if condition is false
|
186
|
-
if exp.condition && !condition_passes(exp.condition)
|
208
|
+
if exp.condition && !condition_passes?(exp.condition)
|
187
209
|
return get_experiment_result(
|
188
210
|
exp,
|
189
211
|
-1,
|
@@ -198,7 +220,10 @@ module Growthbook
|
|
198
220
|
exp.coverage,
|
199
221
|
exp.weights
|
200
222
|
)
|
201
|
-
|
223
|
+
seed = exp.seed || key || ''
|
224
|
+
n = Growthbook::Util.get_hash(seed: seed, value: hash_value, version: exp.hash_version || 1)
|
225
|
+
return get_experiment_result(exp, -1, hash_used: false, feature_id: feature_id) if n.nil?
|
226
|
+
|
202
227
|
assigned = Growthbook::Util.choose_variation(n, ranges)
|
203
228
|
|
204
229
|
# 10. Return if not in experiment
|
@@ -228,7 +253,9 @@ module Growthbook
|
|
228
253
|
new_hash
|
229
254
|
end
|
230
255
|
|
231
|
-
def condition_passes(condition)
|
256
|
+
def condition_passes?(condition)
|
257
|
+
return false if condition.nil?
|
258
|
+
|
232
259
|
Growthbook::Conditions.eval_condition(@attributes, condition)
|
233
260
|
end
|
234
261
|
|
@@ -244,9 +271,18 @@ module Growthbook
|
|
244
271
|
meta = experiment.meta ? experiment.meta[variation_index] : {}
|
245
272
|
|
246
273
|
result = Growthbook::InlineExperimentResult.new(
|
247
|
-
{
|
248
|
-
|
249
|
-
|
274
|
+
{
|
275
|
+
key: meta['key'] || variation_index,
|
276
|
+
in_experiment: in_experiment,
|
277
|
+
variation_id: variation_index,
|
278
|
+
value: experiment.variations[variation_index],
|
279
|
+
hash_used: hash_used,
|
280
|
+
hash_attribute: hash_attribute,
|
281
|
+
hash_value: hash_value,
|
282
|
+
feature_id: feature_id,
|
283
|
+
bucket: bucket,
|
284
|
+
name: meta['name']
|
285
|
+
}
|
250
286
|
)
|
251
287
|
|
252
288
|
result.passthrough = true if meta['passthrough']
|
@@ -254,8 +290,19 @@ module Growthbook
|
|
254
290
|
result
|
255
291
|
end
|
256
292
|
|
257
|
-
def get_feature_result(value, source, experiment
|
258
|
-
Growthbook::FeatureResult.new(value, source, experiment, experiment_result)
|
293
|
+
def get_feature_result(key, value, source, experiment, experiment_result)
|
294
|
+
res = Growthbook::FeatureResult.new(value, source, experiment, experiment_result)
|
295
|
+
|
296
|
+
track_feature_usage(key, res)
|
297
|
+
|
298
|
+
res
|
299
|
+
end
|
300
|
+
|
301
|
+
def track_feature_usage(key, feature_result)
|
302
|
+
return unless on_feature_usage.respond_to?(:on_feature_usage)
|
303
|
+
return if feature_result.source == 'override'
|
304
|
+
|
305
|
+
on_feature_usage.on_feature_usage(key, feature_result)
|
259
306
|
end
|
260
307
|
|
261
308
|
def get_feature(key)
|
@@ -273,8 +320,10 @@ module Growthbook
|
|
273
320
|
end
|
274
321
|
|
275
322
|
def track_experiment(experiment, result)
|
323
|
+
return if listener.nil?
|
324
|
+
|
276
325
|
@listener.on_experiment_viewed(experiment, result) if @listener.respond_to?(:on_experiment_viewed)
|
277
|
-
@impressions[experiment.key] = result
|
326
|
+
@impressions[experiment.key] = result unless experiment.key.nil?
|
278
327
|
end
|
279
328
|
|
280
329
|
def included_in_rollout?(seed:, hash_attribute:, hash_version:, range:, coverage:)
|
@@ -284,7 +333,8 @@ module Growthbook
|
|
284
333
|
|
285
334
|
return false if hash_value.empty?
|
286
335
|
|
287
|
-
n = Growthbook::Util.
|
336
|
+
n = Growthbook::Util.get_hash(seed: seed, value: hash_value, version: hash_version || 1)
|
337
|
+
return false if n.nil?
|
288
338
|
|
289
339
|
return Growthbook::Util.in_range?(n, range) if range
|
290
340
|
return n <= coverage if coverage
|
@@ -299,11 +349,23 @@ module Growthbook
|
|
299
349
|
if hash_value.empty?
|
300
350
|
false
|
301
351
|
else
|
302
|
-
n = Growthbook::Util.
|
352
|
+
n = Growthbook::Util.get_hash(seed: filter['seed'] || '', value: hash_value, version: filter['hashVersion'] || 2)
|
353
|
+
|
354
|
+
return true if n.nil?
|
303
355
|
|
304
356
|
filter['ranges'].none? { |range| Growthbook::Util.in_range?(n, range) }
|
305
357
|
end
|
306
358
|
end
|
307
359
|
end
|
360
|
+
|
361
|
+
def decrypted_features_from_options(options)
|
362
|
+
decrypted_features = DecryptionUtil.decrypt(options[:encrypted_features], key: options[:decryption_key])
|
363
|
+
|
364
|
+
return nil if decrypted_features.nil?
|
365
|
+
|
366
|
+
JSON.parse(decrypted_features)
|
367
|
+
rescue StandardError
|
368
|
+
nil
|
369
|
+
end
|
308
370
|
end
|
309
371
|
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,14 @@ 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'
|
19
|
+
require 'growthbook/feature_usage_callback'
|
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.2.1
|
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-07-13 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,16 @@ 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/feature_usage_callback.rb
|
98
|
+
- lib/growthbook/fnv.rb
|
81
99
|
- lib/growthbook/inline_experiment.rb
|
82
100
|
- lib/growthbook/inline_experiment_result.rb
|
101
|
+
- lib/growthbook/tracking_callback.rb
|
83
102
|
- lib/growthbook/util.rb
|
84
103
|
homepage: https://github.com/growthbook/growthbook-ruby
|
85
104
|
licenses:
|
@@ -101,7 +120,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
101
120
|
- !ruby/object:Gem::Version
|
102
121
|
version: '0'
|
103
122
|
requirements: []
|
104
|
-
rubygems_version: 3.
|
123
|
+
rubygems_version: 3.4.10
|
105
124
|
signing_key:
|
106
125
|
specification_version: 4
|
107
126
|
summary: GrowthBook SDK for Ruby
|