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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 95b90dcba429217cc585f4ea2007e3d7aab0f3489dc93ee41d7fa72b847b7f53
4
- data.tar.gz: 363c48f172798a02d899cfdc6ffb692ab1f85b45cf205e0d4c5dc6339c86c83a
3
+ metadata.gz: cba2938ec675b1626d643fb7493223a54d0faf1ffc37c2bacb5a06637c0a75cd
4
+ data.tar.gz: bce59918f346b53182450ec2fdd92776767954712334bc5b7c0bd48d952c05ce
5
5
  SHA512:
6
- metadata.gz: 6b8c950602e92cb66a644e6780827410d3c2e5eb71078adb95b469fbbe1c9b7f9925a6d3cf5917c345b9a5d04feabf21198d0349a57d5c597e7dca92f004bf43
7
- data.tar.gz: ded0ea4cc0913bf8d93f8f61a34b622579d7346dfe02f121d011e82b11920e7703c192e8afb6331c2ac0d5245a435473e54707907e6116e46d4b4b01a5a3c394
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
 
@@ -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 [Listener] An object that responds to some tracking methods that take experiment and result as arguments.
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
- attr_reader :impressions, :forced_features
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.to_sym
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, hash_attribute: hash_attribute, range: rule.range,
105
- coverage: rule.coverage, hash_version: rule.hash_version
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 || nil, 'defaultValue')
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
- if @url
156
- qs_override = Util.get_query_string_override(key, @url, exp.variations.length)
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
- n = Growthbook::Util.hash(seed: exp.seed || key, value: hash_value, version: exp.hash_version || 1)
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
- { key: meta['key'] || variation_index, in_experiment: in_experiment, variation_id: variation_index,
248
- value: experiment.variations[variation_index], hash_used: hash_used, hash_attribute: hash_attribute,
249
- hash_value: hash_value, feature_id: feature_id, bucket: bucket, name: meta['name'] }
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 = nil, experiment_result = nil)
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.hash(seed: seed, value: hash_value, version: hash_version || 1)
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.hash(seed: filter['seed'], value: hash_value, version: filter['hashVersion'] || 2)
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
@@ -98,7 +98,9 @@ module Growthbook
98
98
  end
99
99
 
100
100
  def experiment?
101
- !!@variations
101
+ return false if @variations.nil?
102
+
103
+ !@variations&.empty?
102
104
  end
103
105
 
104
106
  def force?
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Growthbook
4
+ # Extendable class that can be used as the feature usage callback
5
+ class FeatureUsageCallback
6
+ def on_feature_usage(_feature_key, _result); end
7
+ end
8
+ 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)
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Growthbook
4
+ # Extendable class that can be used as the tracking callback
5
+ class TrackingCallback
6
+ def on_experiment_viewed(_experiment, _result); end
7
+ end
8
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'fnv'
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
- def self.hash(seed:, value:, version:)
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
- -1
50
+ nil
50
51
  end
51
52
 
52
- def self.in_namespace(hash_value, namespace)
53
- n = hash(seed: "__#{namespace[0]}", value: hash_value, version: 1)
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 = 1, weights = [])
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
- return nil unless parsed.query
114
+ parsed_query = parsed.query
115
+ return nil if parsed_query.nil?
110
116
 
111
- qs = URI.decode_www_form(parsed.query)
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.0.0
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-04-20 00:00:00.000000000 Z
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: fnv
70
+ name: webmock
57
71
  requirement: !ruby/object:Gem::Requirement
58
72
  requirements:
59
73
  - - "~>"
60
74
  - !ruby/object:Gem::Version
61
- version: 0.2.0
62
- type: :runtime
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: 0.2.0
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.1.2
123
+ rubygems_version: 3.4.10
105
124
  signing_key:
106
125
  specification_version: 4
107
126
  summary: GrowthBook SDK for Ruby