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 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