growthbook 0.3.0 → 1.1.0

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: 9f6c9a059b34fad58095b5e0ea6b3710d7b427617a4cd4f9c7ddc8fdebd2a1da
4
- data.tar.gz: c93b9e289a8c950dc3cfa5974062634e7c11f6123e7fd5e94f6ff8c88d262566
3
+ metadata.gz: f5919b4bd1e6564233d0fb61f00f2fcc5fee175f3122a35e9815df47ebdaeb6f
4
+ data.tar.gz: 88bf0372e161e261a536fd8953f858e9f4ad60592a96cbfb8e3baa523414cbca
5
5
  SHA512:
6
- metadata.gz: 0f1c46dac7d0e9dccc8641f67d7dbbe8d2af91ec18a4cf0fdcb13701042e0fad55bbcf35b91c330506816fa59cbb0129a8ae5a7f3fb103b308735268f3638937
7
- data.tar.gz: c38fe86529984df54ee57560b01591c99a20b616f02d8296e9e81a7c1fc56d6755096c675e96e94a52c57c5e9e77435b497aed2d4a3ebb4c0cdcbd78f7d8085b
6
+ metadata.gz: 771a1f4fd4eeb6acb92606e67677d65717e3b3a98de80528d2adef3cce17f183a6524af1da74cb09ff18d195a062dd1c882c208cb8dfeaf631603c54b7d128e9
7
+ data.tar.gz: 3b2386b97c64ea3609ad907005d90baec0a25e7b51d45bbd973490d66a204255d5a31d1d435da406cfcd8f3443a28bb8163e6f649af114999b847cbe822f2593
@@ -3,6 +3,8 @@
3
3
  require 'json'
4
4
 
5
5
  module Growthbook
6
+ # internal use only
7
+ # Utils for condition evaluation
6
8
  class Conditions
7
9
  # Evaluate a targeting conditions hash against an attributes hash
8
10
  # Both attributes and conditions only have string keys (no symbols)
@@ -25,9 +27,8 @@ module Growthbook
25
27
  when Array
26
28
  return condition.map { |v| parse_condition(v) }
27
29
  when Hash
28
- return condition.map { |k, v| [k.to_s, parse_condition(v)] }.to_h
30
+ return condition.to_h { |k, v| [k.to_s, parse_condition(v)] }
29
31
  end
30
-
31
32
  condition
32
33
  end
33
34
 
@@ -47,7 +48,7 @@ module Growthbook
47
48
  true
48
49
  end
49
50
 
50
- def self.is_operator_object(obj)
51
+ def self.operator_object?(obj)
51
52
  obj.each do |key, _value|
52
53
  return false if key[0] != '$'
53
54
  end
@@ -58,7 +59,7 @@ module Growthbook
58
59
  return 'string' if attribute_value.is_a? String
59
60
  return 'number' if attribute_value.is_a? Integer
60
61
  return 'number' if attribute_value.is_a? Float
61
- return 'boolean' if attribute_value == true || attribute_value == false
62
+ return 'boolean' if [true, false].include?(attribute_value)
62
63
  return 'array' if attribute_value.is_a? Array
63
64
  return 'null' if attribute_value.nil?
64
65
 
@@ -66,22 +67,22 @@ module Growthbook
66
67
  end
67
68
 
68
69
  def self.get_path(attributes, path)
70
+ path = path.to_s if path.is_a?(Symbol)
71
+
69
72
  parts = path.split('.')
70
73
  current = attributes
71
74
 
72
75
  parts.each do |value|
73
- if current && current.is_a?(Hash) && current.key?(value)
74
- current = current[value]
75
- else
76
- return nil
77
- end
76
+ return nil unless current.is_a?(Hash) && current&.key?(value)
77
+
78
+ current = current[value]
78
79
  end
79
80
 
80
81
  current
81
82
  end
82
83
 
83
84
  def self.eval_condition_value(condition_value, attribute_value)
84
- if condition_value.is_a?(Hash) && is_operator_object(condition_value)
85
+ if condition_value.is_a?(Hash) && operator_object?(condition_value)
85
86
  condition_value.each do |key, value|
86
87
  return false unless eval_operator_condition(key, attribute_value, value)
87
88
  end
@@ -94,7 +95,7 @@ module Growthbook
94
95
  return false unless attribute_value.is_a? Array
95
96
 
96
97
  attribute_value.each do |item|
97
- if is_operator_object(condition)
98
+ if operator_object?(condition)
98
99
  return true if eval_condition_value(condition, item)
99
100
  elsif eval_condition(item, condition)
100
101
  return true
@@ -147,10 +148,10 @@ module Growthbook
147
148
  true
148
149
  when '$exists'
149
150
  exists = !attribute_value.nil?
150
- if !condition_value
151
- !exists
152
- else
151
+ if condition_value
153
152
  exists
153
+ else
154
+ !exists
154
155
  end
155
156
  when '$type'
156
157
  condition_value == get_type(attribute_value)
@@ -1,9 +1,34 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Growthbook
4
+ # Context object passed into the GrowthBook constructor.
4
5
  class Context
5
- attr_accessor :enabled, :url, :qa_mode, :listener
6
- attr_reader :attributes, :features, :impressions, :forced_variations, :forced_features
6
+ # @return [true, false] Switch to globally disable all experiments. Default true.
7
+ attr_accessor :enabled
8
+
9
+ # @return [String] The URL of the current page
10
+ attr_accessor :url
11
+
12
+ # @return [true, false, nil] If true, random assignment is disabled and only explicitly forced variations are used.
13
+ attr_accessor :qa_mode
14
+
15
+ # @return [Growthbook::TrackingCallback] An object that responds to `on_experiment_viewed(GrowthBook::InlineExperiment, GrowthBook::InlineExperimentResult)`
16
+ attr_accessor :listener
17
+
18
+ # @return [Hash] Map of user attributes that are used to assign variations
19
+ attr_reader :attributes
20
+
21
+ # @return [Hash] Feature definitions (usually pulled from an API or cache)
22
+ attr_reader :features
23
+
24
+ # @return [Hash] Force specific experiments to always assign a specific variation (used for QA)
25
+ attr_reader :forced_variations
26
+
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
7
32
 
8
33
  def initialize(options = {})
9
34
  @features = {}
@@ -13,14 +38,19 @@ module Growthbook
13
38
  @enabled = true
14
39
  @impressions = {}
15
40
 
16
- options.each do |key, value|
17
- case key.to_sym
41
+ options.transform_keys(&:to_sym).each do |key, value|
42
+ case key
18
43
  when :enabled
19
44
  @enabled = value
20
45
  when :attributes
21
46
  self.attributes = value
22
47
  when :url
23
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?
24
54
  when :features
25
55
  self.features = value
26
56
  when :forced_variations, :forcedVariations
@@ -40,6 +70,8 @@ module Growthbook
40
70
  def features=(features)
41
71
  @features = {}
42
72
 
73
+ return if features.nil?
74
+
43
75
  features.each do |k, v|
44
76
  # Convert to a Feature object if it's not already
45
77
  v = Growthbook::Feature.new(v) unless v.is_a? Growthbook::Feature
@@ -62,42 +94,49 @@ module Growthbook
62
94
 
63
95
  def eval_feature(key)
64
96
  # Forced in the context
65
- if @forced_features.key?(key.to_s)
66
- return get_feature_result(@forced_features[key.to_s], 'override')
67
- end
97
+ return get_feature_result(@forced_features[key.to_s], 'override', nil, nil) if @forced_features.key?(key.to_s)
68
98
 
69
99
  # Return if we can't find the feature definition
70
100
  feature = get_feature(key)
71
- return get_feature_result(nil, 'unknownFeature') unless feature
101
+ return get_feature_result(nil, 'unknownFeature', nil, nil) unless feature
72
102
 
73
103
  feature.rules.each do |rule|
74
104
  # Targeting condition
75
- next if rule.condition && !condition_passes(rule.condition)
76
-
77
- # Rollout or forced value rule
78
- if rule.is_force?
79
- unless rule.coverage.nil?
80
- hash_value = get_attribute(rule.hash_attribute || 'id').to_s
81
- next if hash_value.length.zero?
82
-
83
- n = Growthbook::Util.hash(hash_value + key)
84
- next if n > rule.coverage
85
- end
86
- return get_feature_result(rule.force, 'force')
105
+ next if rule.condition && !condition_passes?(rule.condition)
106
+
107
+ # If there are filters for who is included (e.g. namespaces)
108
+ next if rule.filters && filtered_out?(rule.filters || [])
109
+
110
+ # If this is a percentage rollout, skip if not included
111
+ if rule.force?
112
+ seed = rule.seed || key
113
+ hash_attribute = rule.hash_attribute || 'id'
114
+ included_in_rollout = included_in_rollout?(
115
+ seed: seed.to_s,
116
+ hash_attribute: hash_attribute,
117
+ range: rule.range,
118
+ coverage: rule.coverage,
119
+ hash_version: rule.hash_version
120
+ )
121
+ next unless included_in_rollout
122
+
123
+ return get_feature_result(rule.force, 'force', nil, nil)
87
124
  end
88
125
  # Experiment rule
89
- next unless rule.is_experiment?
126
+ next unless rule.experiment?
90
127
 
91
128
  exp = rule.to_experiment(key)
129
+ next if exp.nil?
130
+
92
131
  result = _run(exp, key)
93
132
 
94
- next unless result.in_experiment
133
+ next unless result.in_experiment && !result.passthrough
95
134
 
96
135
  return get_feature_result(result.value, 'experiment', exp, result)
97
136
  end
98
137
 
99
138
  # Fallback
100
- get_feature_result(feature.default_value || nil, 'defaultValue')
139
+ get_feature_result(feature.default_value.nil? ? nil : feature.default_value, 'defaultValue', nil, nil)
101
140
  end
102
141
 
103
142
  def run(exp)
@@ -119,58 +158,80 @@ module Growthbook
119
158
 
120
159
  private
121
160
 
122
- def _run(exp, feature_id="")
161
+ def _run(exp, feature_id = '')
123
162
  key = exp.key
124
163
 
125
164
  # 1. If experiment doesn't have enough variations, return immediately
126
- return get_experiment_result(exp, -1, false, feature_id) if exp.variations.length < 2
165
+ return get_experiment_result(exp, -1, hash_used: false, feature_id: feature_id) if exp.variations.length < 2
127
166
 
128
167
  # 2. If context is disabled, return immediately
129
- return get_experiment_result(exp, -1, false, feature_id) unless @enabled
168
+ return get_experiment_result(exp, -1, hash_used: false, feature_id: feature_id) unless @enabled
130
169
 
131
170
  # 3. If forced via URL querystring
132
- if @url
133
- qsOverride = Util.get_query_string_override(key, @url, exp.variations.length)
134
- return get_experiment_result(exp, qsOverride, false, feature_id) unless qsOverride.nil?
171
+ override_url = @url
172
+ unless override_url.nil?
173
+ qs_override = Util.get_query_string_override(key, override_url, exp.variations.length)
174
+ return get_experiment_result(exp, qs_override, hash_used: false, feature_id: feature_id) unless qs_override.nil?
135
175
  end
136
176
 
137
177
  # 4. If variation is forced in the context, return the forced value
138
- return get_experiment_result(exp, @forced_variations[key.to_s], false, feature_id) if @forced_variations.key?(key.to_s)
178
+ if @forced_variations.key?(key.to_s)
179
+ return get_experiment_result(
180
+ exp,
181
+ @forced_variations[key.to_s],
182
+ hash_used: false,
183
+ feature_id: feature_id
184
+ )
185
+ end
139
186
 
140
187
  # 5. Exclude if not active
141
- return get_experiment_result(exp, -1, false, feature_id) unless exp.active
188
+ return get_experiment_result(exp, -1, hash_used: false, feature_id: feature_id) unless exp.active
142
189
 
143
190
  # 6. Get hash_attribute/value and return if empty
144
191
  hash_attribute = exp.hash_attribute || 'id'
145
192
  hash_value = get_attribute(hash_attribute).to_s
146
- return get_experiment_result(exp, -1, false, feature_id) if hash_value.length.zero?
193
+ return get_experiment_result(exp, -1, hash_used: false, feature_id: feature_id) if hash_value.empty?
147
194
 
148
- # 7. Exclude if user not in namespace
149
- return get_experiment_result(exp, -1, false, feature_id) if exp.namespace && !Growthbook::Util.in_namespace(hash_value, exp.namespace)
195
+ # 7. Exclude if user is filtered out (used to be called "namespace")
196
+ if exp.filters
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)
199
+ return get_experiment_result(exp, -1, hash_used: false, feature_id: feature_id)
200
+ end
150
201
 
151
202
  # 8. Exclude if condition is false
152
- return get_experiment_result(exp, -1, false, feature_id) if exp.condition && !condition_passes(exp.condition)
203
+ if exp.condition && !condition_passes?(exp.condition)
204
+ return get_experiment_result(
205
+ exp,
206
+ -1,
207
+ hash_used: false,
208
+ feature_id: feature_id
209
+ )
210
+ end
153
211
 
154
- # 9. Calculate bucket ranges and choose one
155
- ranges = Growthbook::Util.get_bucket_ranges(
212
+ # 9. Get bucket ranges and choose variation
213
+ ranges = exp.ranges || Growthbook::Util.get_bucket_ranges(
156
214
  exp.variations.length,
157
215
  exp.coverage,
158
216
  exp.weights
159
217
  )
160
- n = Growthbook::Util.hash(hash_value + key)
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
+
161
222
  assigned = Growthbook::Util.choose_variation(n, ranges)
162
223
 
163
224
  # 10. Return if not in experiment
164
- return get_experiment_result(exp, -1, false, feature_id) if assigned.negative?
225
+ return get_experiment_result(exp, -1, hash_used: false, feature_id: feature_id) if assigned.negative?
165
226
 
166
227
  # 11. Experiment has a forced variation
167
- return get_experiment_result(exp, exp.force, false, feature_id) unless exp.force.nil?
228
+ return get_experiment_result(exp, exp.force, hash_used: false, feature_id: feature_id) unless exp.force.nil?
168
229
 
169
230
  # 12. Exclude if in QA mode
170
- return get_experiment_result(exp, -1, false, feature_id) if @qa_mode
231
+ return get_experiment_result(exp, -1, hash_used: false, feature_id: feature_id) if @qa_mode
171
232
 
172
233
  # 13. Build the result object
173
- result = get_experiment_result(exp, assigned, true, feature_id)
234
+ result = get_experiment_result(exp, assigned, hash_used: true, feature_id: feature_id, bucket: n)
174
235
 
175
236
  # 14. Fire tracking callback
176
237
  track_experiment(exp, result)
@@ -187,11 +248,13 @@ module Growthbook
187
248
  new_hash
188
249
  end
189
250
 
190
- def condition_passes(condition)
251
+ def condition_passes?(condition)
252
+ return false if condition.nil?
253
+
191
254
  Growthbook::Conditions.eval_condition(@attributes, condition)
192
255
  end
193
256
 
194
- def get_experiment_result(experiment, variation_index = -1, hash_used = false, feature_id = "")
257
+ def get_experiment_result(experiment, variation_index = -1, hash_used: false, feature_id: '', bucket: nil)
195
258
  in_experiment = true
196
259
  if variation_index.negative? || variation_index >= experiment.variations.length
197
260
  variation_index = 0
@@ -200,12 +263,29 @@ module Growthbook
200
263
 
201
264
  hash_attribute = experiment.hash_attribute || 'id'
202
265
  hash_value = get_attribute(hash_attribute)
266
+ meta = experiment.meta ? experiment.meta[variation_index] : {}
267
+
268
+ result = Growthbook::InlineExperimentResult.new(
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
+ }
281
+ )
282
+
283
+ result.passthrough = true if meta['passthrough']
203
284
 
204
- Growthbook::InlineExperimentResult.new(hash_used, in_experiment, variation_index,
205
- experiment.variations[variation_index], hash_attribute, hash_value, feature_id)
285
+ result
206
286
  end
207
287
 
208
- def get_feature_result(value, source, experiment = nil, experiment_result = nil)
288
+ def get_feature_result(value, source, experiment, experiment_result)
209
289
  Growthbook::FeatureResult.new(value, source, experiment, experiment_result)
210
290
  end
211
291
 
@@ -224,10 +304,52 @@ module Growthbook
224
304
  end
225
305
 
226
306
  def track_experiment(experiment, result)
227
- if @listener && @listener.respond_to?(:on_experiment_viewed)
228
- @listener.on_experiment_viewed(experiment, result)
307
+ return if listener.nil?
308
+
309
+ @listener.on_experiment_viewed(experiment, result) if @listener.respond_to?(:on_experiment_viewed)
310
+ @impressions[experiment.key] = result unless experiment.key.nil?
311
+ end
312
+
313
+ def included_in_rollout?(seed:, hash_attribute:, hash_version:, range:, coverage:)
314
+ return true if range.nil? && coverage.nil?
315
+
316
+ hash_value = get_attribute(hash_attribute)
317
+
318
+ return false if hash_value.empty?
319
+
320
+ n = Growthbook::Util.get_hash(seed: seed, value: hash_value, version: hash_version || 1)
321
+ return false if n.nil?
322
+
323
+ return Growthbook::Util.in_range?(n, range) if range
324
+ return n <= coverage if coverage
325
+
326
+ true
327
+ end
328
+
329
+ def filtered_out?(filters)
330
+ filters.any? do |filter|
331
+ hash_value = get_attribute(filter['attribute'] || 'id')
332
+
333
+ if hash_value.empty?
334
+ false
335
+ else
336
+ n = Growthbook::Util.get_hash(seed: filter['seed'] || '', value: hash_value, version: filter['hashVersion'] || 2)
337
+
338
+ return true if n.nil?
339
+
340
+ filter['ranges'].none? { |range| Growthbook::Util.in_range?(n, range) }
341
+ end
229
342
  end
230
- @impressions[experiment.key] = result
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
231
353
  end
232
354
  end
233
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
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Growthbook
4
+ # The feature with a generic value type.
4
5
  class Feature
5
6
  # @return [Any , nil]
6
7
  attr_reader :default_value
@@ -9,9 +10,9 @@ module Growthbook
9
10
  attr_reader :rules
10
11
 
11
12
  def initialize(feature)
12
- @default_value = getOption(feature, :defaultValue)
13
+ @default_value = get_option(feature, :defaultValue)
13
14
 
14
- rules = getOption(feature, :rules)
15
+ rules = get_option(feature, :rules)
15
16
 
16
17
  @rules = []
17
18
  rules&.each do |rule|
@@ -31,7 +32,7 @@ module Growthbook
31
32
 
32
33
  private
33
34
 
34
- def getOption(hash, key)
35
+ def get_option(hash, key)
35
36
  return hash[key.to_sym] if hash.key?(key.to_sym)
36
37
  return hash[key.to_s] if hash.key?(key.to_s)
37
38
 
@@ -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
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Growthbook
4
+ # Result of a feature evaluation
4
5
  class FeatureResult
5
6
  # The assigned value of the feature
6
7
  # @return [Any, nil]
@@ -19,11 +20,11 @@ module Growthbook
19
20
  attr_reader :source
20
21
 
21
22
  # The experiment used to decide the feature value
22
- # @return [Growthbook.Experiment, nil]
23
+ # @return [Growthbook::InlineExperiment, nil]
23
24
  attr_reader :experiment
24
25
 
25
26
  # The result of the experiment
26
- # @return [Growthbook.ExperimentResult, nil]
27
+ # @return [Growthbook::InlineExperimentResult, nil]
27
28
  attr_reader :experiment_result
28
29
 
29
30
  def initialize(