growthbook 1.0.0 → 1.2.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 +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
|