growthbook 1.0.0 → 1.1.0
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 +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:
|