growthbook 0.3.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,37 +1,82 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Growthbook
4
+ # Internal class that overrides the default value of a Feature based on a set of requirements.
4
5
  class FeatureRule
5
- # @return [Hash , nil]
6
+ # @return [Hash , nil] Optional targeting condition
6
7
  attr_reader :condition
7
- # @return [Float , nil]
8
+
9
+ # @return [Float , nil] What percent of users should be included in the experiment (between 0 and 1, inclusive)
8
10
  attr_reader :coverage
9
- # @return [T , nil]
11
+
12
+ # @return [T , nil] Immediately force a specific value (ignore every other option besides condition and coverage)
10
13
  attr_reader :force
11
- # @return [T[] , nil]
14
+
15
+ # @return [T[] , nil] Run an experiment (A/B test) and randomly choose between these variations
12
16
  attr_reader :variations
13
- # @return [String , nil]
17
+
18
+ # @return [String , nil] The globally unique tracking key for the experiment (default to the feature key)
14
19
  attr_reader :key
15
- # @return [Float[] , nil]
20
+
21
+ # @return [Float[] , nil] How to weight traffic between variations. Must add to 1.
16
22
  attr_reader :weights
17
- # @return [Array , nil]
23
+
24
+ # @return [String , nil] Adds the experiment to a namespace
18
25
  attr_reader :namespace
19
- # @return [String , nil]
26
+
27
+ # @return [String , nil] What user attribute should be used to assign variations (defaults to id)
20
28
  attr_reader :hash_attribute
21
29
 
30
+ # @return [Integer , nil] The hash version to use (default to 1)
31
+ attr_reader :hash_version
32
+
33
+ # @return [BucketRange , nil] A more precise version of coverage
34
+ attr_reader :range
35
+
36
+ # @return [BucketRanges[] , nil] Ranges for experiment variations
37
+ attr_reader :ranges
38
+
39
+ # @return [VariationMeta[] , nil] Meta info about the experiment variations
40
+ attr_reader :meta
41
+
42
+ # @return [Filter[] , nil] Array of filters to apply to the rule
43
+ attr_reader :filters
44
+
45
+ # @return [String , nil] Seed to use for hashing
46
+ attr_reader :seed
47
+
48
+ # @return [String , nil] Human-readable name for the experiment
49
+ attr_reader :name
50
+
51
+ # @return [String , nil] The phase id of the experiment
52
+ attr_reader :phase
53
+
54
+ # @return [TrackData[] , nil] Array of tracking calls to fire
55
+ attr_reader :tracks
56
+
22
57
  def initialize(rule)
23
- @coverage = getOption(rule, :coverage)
24
- @force = getOption(rule, :force)
25
- @variations = getOption(rule, :variations)
26
- @key = getOption(rule, :key)
27
- @weights = getOption(rule, :weights)
28
- @namespace = getOption(rule, :namespace)
29
- @hash_attribute = getOption(rule, :hash_attribute) || getOption(rule, :hashAttribute)
30
-
31
- cond = getOption(rule, :condition)
58
+ @coverage = get_option(rule, :coverage)
59
+ @force = get_option(rule, :force)
60
+ @variations = get_option(rule, :variations)
61
+ @key = get_option(rule, :key)
62
+ @weights = get_option(rule, :weights)
63
+ @namespace = get_option(rule, :namespace)
64
+ @hash_attribute = get_option(rule, :hash_attribute) || get_option(rule, :hashAttribute)
65
+ @hash_version = get_option(rule, :hash_version) || get_option(rule, :hashVersion)
66
+ @range = get_option(rule, :range)
67
+ @ranges = get_option(rule, :ranges)
68
+ @meta = get_option(rule, :meta)
69
+ @filters = get_option(rule, :filters)
70
+ @seed = get_option(rule, :seed)
71
+ @name = get_option(rule, :name)
72
+ @phase = get_option(rule, :phase)
73
+ @tracks = get_option(rule, :tracks)
74
+
75
+ cond = get_option(rule, :condition)
32
76
  @condition = Growthbook::Conditions.parse_condition(cond) unless cond.nil?
33
77
  end
34
78
 
79
+ # @return [Growthbook::InlineExperiment, nil]
35
80
  def to_experiment(feature_key)
36
81
  return nil unless @variations
37
82
 
@@ -41,34 +86,51 @@ module Growthbook
41
86
  coverage: @coverage,
42
87
  weights: @weights,
43
88
  hash_attribute: @hash_attribute,
44
- namespace: @namespace
89
+ hash_version: @hash_version,
90
+ namespace: @namespace,
91
+ meta: @meta,
92
+ ranges: @ranges,
93
+ filters: @filters,
94
+ name: @name,
95
+ phase: @phase,
96
+ seed: @seed
45
97
  )
46
98
  end
47
99
 
48
- def is_experiment?
49
- !!@variations
100
+ def experiment?
101
+ return false if @variations.nil?
102
+
103
+ !@variations&.empty?
50
104
  end
51
105
 
52
- def is_force?
53
- !is_experiment? && !@force.nil?
106
+ def force?
107
+ !experiment? && !@force.nil?
54
108
  end
55
109
 
56
110
  def to_json(*_args)
57
- res = {}
58
- res['condition'] = @condition unless @condition.nil?
59
- res['coverage'] = @coverage unless @coverage.nil?
60
- res['force'] = @force unless @force.nil?
61
- res['variations'] = @variations unless @variations.nil?
62
- res['key'] = @key unless @key.nil?
63
- res['weights'] = @weights unless @weights.nil?
64
- res['namespace'] = @namespace unless @namespace.nil?
65
- res['hashAttribute'] = @hash_attribute unless @hash_attribute.nil?
66
- res
111
+ {
112
+ 'condition' => @condition,
113
+ 'coverage' => @coverage,
114
+ 'force' => @force,
115
+ 'variations' => @variations,
116
+ 'key' => @key,
117
+ 'weights' => @weights,
118
+ 'namespace' => @namespace,
119
+ 'hashAttribute' => @hash_attribute,
120
+ 'range' => @range,
121
+ 'ranges' => @ranges,
122
+ 'meta' => @meta,
123
+ 'filters' => @filters,
124
+ 'seed' => @seed,
125
+ 'name' => @name,
126
+ 'phase' => @phase,
127
+ 'tracks' => @tracks
128
+ }.compact
67
129
  end
68
130
 
69
131
  private
70
132
 
71
- def getOption(hash, key)
133
+ def get_option(hash, key)
72
134
  return hash[key.to_sym] if hash.key?(key.to_sym)
73
135
  return hash[key.to_s] if hash.key?(key.to_s)
74
136
 
@@ -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
@@ -1,80 +1,103 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Growthbook
4
+ # Class for creating an inline experiment for evaluating
4
5
  class InlineExperiment
5
- # @returns [String]
6
+ # @return [String] The globally unique identifier for the experiment
6
7
  attr_accessor :key
7
8
 
8
- # @returns [Any]
9
+ # @return [Array<String, Integer, Hash>] The different variations to choose between
9
10
  attr_accessor :variations
10
11
 
11
- # @returns [Bool]
12
- attr_accessor :active
13
-
14
- # @returns [Integer, nil]
15
- attr_accessor :force
16
-
17
- # @returns [Array<Float>, nil]
12
+ # @return [Array<Float>] How to weight traffic between variations. Must add to 1.
18
13
  attr_accessor :weights
19
14
 
20
- # @returns [Float]
15
+ # @return [true, false] If set to false, always return the control (first variation)
16
+ attr_accessor :active
17
+
18
+ # @return [Float] What percent of users should be included in the experiment (between 0 and 1, inclusive)
21
19
  attr_accessor :coverage
22
20
 
23
- # @returns [Hash, nil]
21
+ # @return [Array<Hash>] Array of ranges, one per variation
22
+ attr_accessor :ranges
23
+
24
+ # @return [Hash] Optional targeting condition
24
25
  attr_accessor :condition
25
26
 
26
- # @returns [Array]
27
+ # @return [String, nil] Adds the experiment to a namespace
27
28
  attr_accessor :namespace
28
29
 
29
- # @returns [String]
30
+ # @return [integer, nil] All users included in the experiment will be forced into the specific variation index
31
+ attr_accessor :force
32
+
33
+ # @return [String] What user attribute should be used to assign variations (defaults to id)
30
34
  attr_accessor :hash_attribute
31
35
 
32
- # Constructor for an Experiment
33
- #
34
- # @param options [Hash]
35
- # @option options [Array<Any>] :variations The variations to pick between
36
- # @option options [String] :key The unique identifier for this experiment
37
- # @option options [Float] :coverage (1.0) The percent of elegible traffic to include in the experiment
38
- # @option options [Array<Float>] :weights The relative weights of the variations.
39
- # Length must be the same as the number of variations. Total should add to 1.0.
40
- # Default is an even split between variations
41
- # @option options [Boolean] :anon (false) If false, the experiment uses the logged-in user id for bucketing
42
- # If true, the experiment uses the anonymous id for bucketing
43
- # @option options [Array<String>] :targeting Array of targeting rules in the format "key op value"
44
- # where op is one of: =, !=, <, >, ~, !~
45
- # @option options [Integer, nil] :force If an integer, force all users to get this variation
46
- # @option options [Hash] :data Data to attach to the variations
47
- def initialize(options = {})
48
- @key = getOption(options, :key, '').to_s
49
- @variations = getOption(options, :variations, [])
50
- @active = getOption(options, :active, true)
51
- @force = getOption(options, :force)
52
- @weights = getOption(options, :weights)
53
- @coverage = getOption(options, :coverage, 1)
54
- @condition = getOption(options, :condition)
55
- @namespace = getOption(options, :namespace)
56
- @hash_attribute = getOption(options, :hash_attribute) || getOption(options, :hashAttribute) || 'id'
57
- end
36
+ # @return [Integer] The hash version to use (default to 1)
37
+ attr_accessor :hash_version
58
38
 
59
- def getOption(hash, key, default = nil)
60
- return hash[key.to_sym] if hash.key?(key.to_sym)
61
- return hash[key.to_s] if hash.key?(key.to_s)
39
+ # @return [Array<Hash>] Meta info about the variations
40
+ attr_accessor :meta
62
41
 
63
- default
42
+ # @return [Array<Hash>] Array of filters to apply
43
+ attr_accessor :filters
44
+
45
+ # @return [String, nil] The hash seed to use
46
+ attr_accessor :seed
47
+
48
+ # @return [String] Human-readable name for the experiment
49
+ attr_accessor :name
50
+
51
+ # @return [String, nil] Id of the current experiment phase
52
+ attr_accessor :phase
53
+
54
+ def initialize(options = {})
55
+ @key = get_option(options, :key, '').to_s
56
+ @variations = get_option(options, :variations, [])
57
+ @weights = get_option(options, :weights)
58
+ @active = get_option(options, :active, true)
59
+ @coverage = get_option(options, :coverage, 1.0)
60
+ @ranges = get_option(options, :ranges)
61
+ @condition = get_option(options, :condition)
62
+ @namespace = get_option(options, :namespace)
63
+ @force = get_option(options, :force)
64
+ @hash_attribute = get_option(options, :hash_attribute) || get_option(options, :hashAttribute) || 'id'
65
+ @hash_version = get_option(options, :hash_version) || get_option(options, :hashVersion)
66
+ @meta = get_option(options, :meta)
67
+ @filters = get_option(options, :filters)
68
+ @seed = get_option(options, :seed)
69
+ @name = get_option(options, :name)
70
+ @phase = get_option(options, :phase)
64
71
  end
65
72
 
66
73
  def to_json(*_args)
67
74
  res = {}
68
75
  res['key'] = @key
69
76
  res['variations'] = @variations
70
- res['active'] = @active if @active != true && !@active.nil?
71
- res['force'] = @force unless @force.nil?
72
77
  res['weights'] = @weights unless @weights.nil?
78
+ res['active'] = @active if @active != true && !@active.nil?
73
79
  res['coverage'] = @coverage if @coverage != 1 && !@coverage.nil?
74
- res['condition'] = @condition unless @condition.nil?
75
- res['namespace'] = @namespace unless @namespace.nil?
80
+ res['ranges'] = @ranges
81
+ res['condition'] = @condition
82
+ res['namespace'] = @namespace
83
+ res['force'] = @force unless @force.nil?
76
84
  res['hashAttribute'] = @hash_attribute if @hash_attribute != 'id' && !@hash_attribute.nil?
77
- res
85
+ res['hashVersion'] = @hash_version
86
+ res['meta'] = @meta
87
+ res['filters'] = @filters
88
+ res['seed'] = @seed
89
+ res['name'] = @name
90
+ res['phase'] = @phase
91
+ res.compact
92
+ end
93
+
94
+ private
95
+
96
+ def get_option(hash, key, default = nil)
97
+ return hash[key.to_sym] if hash.key?(key.to_sym)
98
+ return hash[key.to_s] if hash.key?(key.to_s)
99
+
100
+ default
78
101
  end
79
102
  end
80
103
  end
@@ -1,62 +1,81 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Growthbook
4
+ # Result of running an experiment.
4
5
  class InlineExperimentResult
5
- # Whether or not the user is in the experiment
6
- # @return [Bool]
6
+ # @return [Boolean] Whether or not the user is part of the experiment
7
7
  attr_reader :in_experiment
8
8
 
9
- # The array index of the assigned variation
10
- # @return [Integer]
9
+ # @return [Integer] The array index of the assigned variation
11
10
  attr_reader :variation_id
12
11
 
13
- # The assigned variation value
14
- # @return [Any]
12
+ # @return [Any] The array value of the assigned variation
15
13
  attr_reader :value
16
14
 
17
- # If the variation was randomly assigned based on user attribute hashes
18
- # @return [Bool]
15
+ # @return [Bool] If a hash was used to assign a variation
19
16
  attr_reader :hash_used
20
17
 
21
- # The attribute used to split traffic
22
- # @return [String]
18
+ # @return [String] The user attribute used to assign a variation
23
19
  attr_reader :hash_attribute
24
20
 
25
- # The value of the hashAttribute
26
- # @return [String]
21
+ # @return [String] The value of that attribute
27
22
  attr_reader :hash_value
28
23
 
24
+ # @return [String, nil] The id of the feature (if any) that the experiment came from
29
25
  attr_reader :feature_id
30
26
 
31
- def initialize(
32
- hash_used,
33
- in_experiment,
34
- variation_id,
35
- value,
36
- hash_attribute,
37
- hash_value,
38
- feature_id
39
- )
40
-
41
- @hash_used = hash_used
42
- @in_experiment = in_experiment
43
- @variation_id = variation_id
44
- @value = value
45
- @hash_attribute = hash_attribute
46
- @hash_value = hash_value
47
- @feature_id = feature_id
27
+ # @return [String] The unique key for the assigned variation
28
+ attr_reader :key
29
+
30
+ # @return [Float] The hash value used to assign a variation (float from 0 to 1)
31
+ attr_reader :bucket
32
+
33
+ # @return [String , nil] Human-readable name for the experiment
34
+ attr_reader :name
35
+
36
+ # @return [Boolean] Used for holdout groups
37
+ attr_accessor :passthrough
38
+
39
+ def initialize(options = {})
40
+ @key = options[:key]
41
+ @in_experiment = options[:in_experiment]
42
+ @variation_id = options[:variation_id]
43
+ @value = options[:value]
44
+ @hash_used = options[:hash_used]
45
+ @hash_attribute = options[:hash_attribute]
46
+ @hash_value = options[:hash_value]
47
+ @feature_id = options[:feature_id]
48
+ @bucket = options[:bucket]
49
+ @name = options[:name]
50
+ @passthrough = options[:passthrough]
51
+ end
52
+
53
+ # If the variation was randomly assigned based on user attribute hashes
54
+ # @return [Bool]
55
+ def hash_used?
56
+ @hash_used
57
+ end
58
+
59
+ # Whether or not the user is in the experiment
60
+ # @return [Bool]
61
+ def in_experiment?
62
+ @in_experiment
48
63
  end
49
64
 
50
65
  def to_json(*_args)
51
- res = {}
52
- res['inExperiment'] = @in_experiment
53
- res['hashUsed'] = @hash_used
54
- res['variationId'] = @variation_id
55
- res['value'] = @value
56
- res['hashAttribute'] = @hash_attribute
57
- res['hashValue'] = @hash_value
58
- res['featureId'] = @feature_id.to_s
59
- res
66
+ {
67
+ 'inExperiment' => @in_experiment,
68
+ 'variationId' => @variation_id,
69
+ 'value' => @value,
70
+ 'hashUsed' => @hash_used,
71
+ 'hashAttribute' => @hash_attribute,
72
+ 'hashValue' => @hash_value,
73
+ 'featureId' => @feature_id.to_s,
74
+ 'key' => @key.to_s,
75
+ 'bucket' => @bucket,
76
+ 'name' => @name,
77
+ 'passthrough' => @passthrough
78
+ }.compact
60
79
  end
61
80
  end
62
81
  end
@@ -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,10 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'fnv'
3
+ require 'base64'
4
+ require 'bigdecimal'
5
+ require 'bigdecimal/util'
4
6
 
5
7
  module Growthbook
8
+ # internal use only
6
9
  class Util
7
- def self.checkRule(actual, op, desired)
10
+ def self.check_rule(actual, op, desired)
8
11
  # Check if both strings are numeric so we can do natural ordering
9
12
  # for greater than / less than operators
10
13
  numeric = begin
@@ -15,9 +18,9 @@ module Growthbook
15
18
 
16
19
  case op
17
20
  when '='
18
- numeric ? Float(actual) == Float(desired) : actual == desired
21
+ numeric ? Float(actual).to_d == Float(desired).to_d : actual == desired
19
22
  when '!='
20
- numeric ? Float(actual) != Float(desired) : actual != desired
23
+ numeric ? Float(actual).to_d != Float(desired).to_d : actual != desired
21
24
  when '>'
22
25
  numeric ? Float(actual) > Float(desired) : actual > desired
23
26
  when '<'
@@ -39,100 +42,86 @@ module Growthbook
39
42
  end
40
43
  end
41
44
 
42
- def self.chooseVariation(userId, experiment)
43
- testId = experiment.id
44
- weights = experiment.getScaledWeights
45
+ # @return [Float, nil] Hash, or nil if the hash version is invalid
46
+ def self.get_hash(seed:, value:, version:)
47
+ return (FNV.new.fnv1a_32(value + seed) % 1000) / 1000.0 if version == 1
48
+ return (FNV.new.fnv1a_32(FNV.new.fnv1a_32(seed + value).to_s) % 10_000) / 10_000.0 if version == 2
45
49
 
46
- # Hash the user id and testName to a number from 0 to 1
47
- n = (FNV.new.fnv1a_32(userId + testId) % 1000) / 1000.0
48
-
49
- cumulativeWeight = 0
50
-
51
- match = -1
52
- i = 0
53
- weights.each do |weight|
54
- cumulativeWeight += weight
55
- if n < cumulativeWeight
56
- match = i
57
- break
58
- end
59
- i += 1
60
- end
61
-
62
- match
50
+ nil
63
51
  end
64
52
 
65
- def self.hash(str)
66
- (FNV.new.fnv1a_32(str) % 1000) / 1000.0
67
- end
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?
68
58
 
69
- def self.in_namespace(userId, namespace)
70
- n = hash("#{userId}__#{namespace[0]}")
71
59
  n >= namespace[1] && n < namespace[2]
72
60
  end
73
61
 
74
- def self.get_equal_weights(numVariations)
75
- return [] if numVariations < 1
62
+ def self.get_equal_weights(num_variations)
63
+ return [] if num_variations < 1
76
64
 
77
65
  weights = []
78
- (1..numVariations).each do |_i|
79
- weights << (1.0 / numVariations)
66
+ (1..num_variations).each do |_i|
67
+ weights << (1.0 / num_variations)
80
68
  end
81
69
  weights
82
70
  end
83
71
 
84
72
  # Determine bucket ranges for experiment variations
85
- def self.get_bucket_ranges(numVariations, coverage = 1, weights = [])
73
+ def self.get_bucket_ranges(num_variations, coverage, weights)
86
74
  # Make sure coverage is within bounds
87
- coverage = 1 if coverage.nil?
88
- coverage = 0 if coverage.negative?
89
- 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
90
78
 
91
79
  # Default to equal weights
92
- weights = get_equal_weights(numVariations) if !weights || weights.length != numVariations
80
+ weights = get_equal_weights(num_variations) if !weights || weights.length != num_variations
93
81
 
94
82
  # If weights don't add up to 1 (or close to it), default to equal weights
95
83
  total = weights.sum
96
- weights = get_equal_weights(numVariations) if total < 0.99 || total > 1.01
84
+ weights = get_equal_weights(num_variations) if total < 0.99 || total > 1.01
97
85
 
98
86
  # Convert weights to ranges
99
- cumulative = 0
87
+ cumulative = 0.0
100
88
  ranges = []
101
89
  weights.each do |w|
102
90
  start = cumulative
103
91
  cumulative += w
104
- ranges << [start, start + coverage * w]
92
+ ranges << [start, start + (coverage * w)]
105
93
  end
106
94
 
107
95
  ranges
108
96
  end
109
97
 
110
98
  # Chose a variation based on a hash and range
111
- def self.choose_variation(n, ranges)
99
+ def self.choose_variation(num, ranges)
112
100
  ranges.each_with_index do |range, i|
113
- return i if n >= range[0] && n < range[1]
101
+ return i if num >= range[0] && num < range[1]
114
102
  end
115
103
  -1
116
104
  end
117
105
 
118
106
  # Get an override variation from a url querystring
119
107
  # e.g. http://localhost?my-test=1 will return `1` for id `my-test`
120
- def self.get_query_string_override(id, url, numVariations)
108
+ def self.get_query_string_override(id, url, num_variations)
121
109
  # Skip if url is empty
122
- return nil if url == ''
110
+ return nil if url == '' || id.nil?
123
111
 
124
112
  # Parse out the query string
125
113
  parsed = URI(url)
126
- return nil unless parsed.query
114
+ parsed_query = parsed.query
115
+ return nil if parsed_query.nil?
127
116
 
128
- qs = URI.decode_www_form(parsed.query)
117
+ qs = URI.decode_www_form(parsed_query)
129
118
 
130
119
  # Look for `id` in the querystring and get the value
131
120
  vals = qs.assoc(id)
132
121
  return nil unless vals
133
122
 
134
123
  val = vals.last
135
- return nill unless val
124
+ return nil unless val
136
125
 
137
126
  # Parse the value as an integer
138
127
  n = begin
@@ -144,9 +133,13 @@ module Growthbook
144
133
  # Make sure the integer is within range
145
134
  return nil if n.nil?
146
135
  return nil if n.negative?
147
- return nil if n >= numVariations
136
+ return nil if n >= num_variations
148
137
 
149
138
  n
150
139
  end
140
+
141
+ def self.in_range?(num, range)
142
+ num >= range[0] && num < range[1]
143
+ end
151
144
  end
152
145
  end
data/lib/growthbook.rb CHANGED
@@ -1,18 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # The GrowthBook SDK
3
4
  module Growthbook
4
5
  end
5
6
 
6
- require 'growthbook/client'
7
7
  require 'growthbook/conditions'
8
8
  require 'growthbook/context'
9
- require 'growthbook/experiment'
10
- require 'growthbook/experiment_result'
9
+ require 'growthbook/decryption_util'
11
10
  require 'growthbook/feature'
11
+ require 'growthbook/feature_repository'
12
12
  require 'growthbook/feature_result'
13
13
  require 'growthbook/feature_rule'
14
+ require 'growthbook/fnv'
14
15
  require 'growthbook/inline_experiment'
15
16
  require 'growthbook/inline_experiment_result'
16
- require 'growthbook/lookup_result'
17
- require 'growthbook/user'
17
+ require 'growthbook/tracking_callback'
18
18
  require 'growthbook/util'