growthbook 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 91514b6437d92dc4d5ff6464f6ae4088dd2f57974fe60ac24d001eeca58ca498
4
+ data.tar.gz: 5ecb57a051072fd053838e8dfb0733bf879a44eddb68687583f1c7245496aae6
5
+ SHA512:
6
+ metadata.gz: 77a2244bec748a0d55666716b0e532430fc84c3f0d268bf44a54e60fd19ea92329af4cb2cdd19e9af90606845ae8d6a7db6d7ca066d1e1ee2af8a2b126593280
7
+ data.tar.gz: af03f9ed8278aaf28705b3eed68e81e9fe0f58e482e476ecfc8bc00c073663db68dcd7f70e2902be56a2debf2eb0e465ac29c6730d2b6377f71c862ee924b8b7
@@ -0,0 +1,9 @@
1
+ module Growthbook
2
+ end
3
+
4
+ require 'growthbook/client'
5
+ require 'growthbook/experiment_result'
6
+ require 'growthbook/lookup_result'
7
+ require 'growthbook/experiment'
8
+ require 'growthbook/util'
9
+ require 'growthbook/user'
@@ -0,0 +1,67 @@
1
+ module Growthbook
2
+ class Client
3
+ # @returns [Boolean]
4
+ attr_accessor :enabled
5
+
6
+ # @returns [Array<Growthbook::Experiment>]
7
+ attr_accessor :experiments
8
+
9
+ # @param config [Hash]
10
+ # @option config [Boolean] :enabled (true) Set to false to disable all experiments
11
+ # @option config [Array<Growthbook::Experiment>] :experiments ([]) Array of Growthbook::Experiment objects
12
+ def initialize(config = {})
13
+ @enabled = config.has_key?(:enabled) ? config[:enabled] : true
14
+ @experiments = config[:experiments] || []
15
+ @resultsToTrack = []
16
+ end
17
+
18
+ # Look up a pre-configured experiment by id
19
+ #
20
+ # @param id [String] The experiment id to look up
21
+ # @return [Growthbook::Experiment, nil] the experiment object or nil if not found
22
+ def getExperiment(id)
23
+ match = nil;
24
+ @experiments.each do |exp|
25
+ if exp.id == id
26
+ match = exp
27
+ break
28
+ end
29
+ end
30
+ return match
31
+ end
32
+
33
+ # Get a User object you can run experiments against
34
+ #
35
+ # @param params [Hash]
36
+ # @option params [String, nil] :id The logged-in user id
37
+ # @option params [String, nil] :anonId The anonymous id (session id, ip address, cookie, etc.)
38
+ # @option params [Hash, nil] :attributes Any user attributes you want to use for experiment targeting
39
+ # Values can be any type, even nested arrays and hashes
40
+ # @return [Growthbook::User] the User object
41
+ def user(params = {})
42
+ Growthbook::User.new(
43
+ params[:anonId] || nil,
44
+ params[:id] || nil,
45
+ params[:attributes] || nil,
46
+ self
47
+ )
48
+ end
49
+
50
+ def importExperimentsHash(experimentsHash = {})
51
+ @experiments = []
52
+ experimentsHash.each do |id, data|
53
+ variations = data["variations"]
54
+
55
+ options = {}
56
+ options[:coverage] = data["coverage"] if data.has_key?("coverage")
57
+ options[:weights] = data["weights"] if data.has_key?("weights")
58
+ options[:force] = data["force"] if data.has_key?("force")
59
+ options[:anon] = data["anon"] if data.has_key?("anon")
60
+ options[:targeting] = data["targeting"] if data.has_key?("targeting")
61
+ options[:data] = data["data"] if data.has_key?("data")
62
+
63
+ @experiments << Growthbook::Experiment.new(id, variations, options)
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,72 @@
1
+ module Growthbook
2
+ class Experiment
3
+ # @returns [String]
4
+ attr_accessor :id
5
+
6
+ # @returns [Integer]
7
+ attr_accessor :variations
8
+
9
+ # @returns [Float]
10
+ attr_accessor :coverage
11
+
12
+ # @returns [Array<Float>]
13
+ attr_accessor :weights
14
+
15
+ # @returns [Boolean]
16
+ attr_accessor :anon
17
+
18
+ # @returns [Array<String>]
19
+ attr_accessor :targeting
20
+
21
+ # @returns [Integer, nil]
22
+ attr_accessor :force
23
+
24
+ # @returns [Hash]
25
+ attr_accessor :data
26
+
27
+ # Constructor for an Experiment
28
+ #
29
+ # @param id [String] The unique id for this experiment
30
+ # @param variations [Integer] The number of variations in this experiment (including the Control)
31
+ # @param options [Hash]
32
+ # @option options [Float] :coverage (1.0) The percent of elegible traffic to include in the experiment
33
+ # @option options [Array<Float>] :weights The relative weights of the variations.
34
+ # Length must be the same as the number of variations. Total should add to 1.0.
35
+ # Default is an even split between variations
36
+ # @option options [Boolean] :anon (false) If false, the experiment uses the logged-in user id for bucketing
37
+ # If true, the experiment uses the anonymous id for bucketing
38
+ # @option options [Array<String>] :targeting Array of targeting rules in the format "key op value"
39
+ # where op is one of: =, !=, <, >, ~, !~
40
+ # @option options [Integer, nil] :force If an integer, force all users to get this variation
41
+ # @option options [Hash] :data Data to attach to the variations
42
+ def initialize(id, variations, options = {})
43
+ @id = id
44
+ @variations = variations
45
+ @coverage = options[:coverage] || 1
46
+ @weights = options[:weights] || getEqualWeights()
47
+ @force = options.has_key?(:force) ? options[:force] : nil
48
+ @anon = options.has_key?(:anon) ? options[:anon] : false
49
+ @targeting = options[:targeting] || []
50
+ @data = options[:data] || {}
51
+ end
52
+
53
+ def getScaledWeights
54
+ scaled = @weights.map do |n|
55
+ n*@coverage
56
+ end
57
+
58
+ return scaled
59
+ end
60
+
61
+ private
62
+
63
+ def getEqualWeights
64
+ weights = []
65
+ n = @variations
66
+ for i in 1..n
67
+ weights << (1.0 / n)
68
+ end
69
+ return weights
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,43 @@
1
+ module Growthbook
2
+ class ExperimentResult
3
+ # The experiment that was performed
4
+ # @return [Growthbook::Experiment, nil] If nil, then the experiment with the required id could not be found
5
+ attr_reader :experiment
6
+
7
+ # The user that was experimented on
8
+ # @return [Growthbook::User]
9
+ attr_reader :user
10
+
11
+ # The chosen variation. -1 for "not in experiment", 0 for control, 1 for 1st variation, etc.
12
+ # @return [Integer]
13
+ attr_reader :variation
14
+
15
+ # The data tied to the chosen variation
16
+ # @return [Hash]
17
+ attr_reader :data
18
+
19
+ @forced = false
20
+
21
+ def forced?
22
+ @forced
23
+ end
24
+
25
+ def shouldTrack?
26
+ !@forced && @variation >= 0
27
+ end
28
+
29
+ def initialize(user = nil, experiment = nil, variation = -1, forced = false)
30
+ @experiment = experiment
31
+ @variation = variation
32
+ @forced = forced
33
+
34
+ @data = {}
35
+ if experiment && experiment.data
36
+ var = variation < 0 ? 0 : variation
37
+ experiment.data.each do |k, v|
38
+ @data[k] = v[var]
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,44 @@
1
+ module Growthbook
2
+ class LookupResult
3
+ # The first matching experiment
4
+ # @return [Growthbook::Experiment]
5
+ attr_reader :experiment
6
+
7
+ # The chosen variation. -1 for "not in experiment", 0 for control, 1 for 1st variation, etc.
8
+ # @return [Integer]
9
+ attr_reader :variation
10
+
11
+ # The data tied to the chosen variation
12
+ # @return [Hash]
13
+ attr_reader :data
14
+
15
+ # The value of the data key that was used to lookup the experiment
16
+ attr_reader :value
17
+
18
+ @forced
19
+
20
+ def forced?
21
+ @forced
22
+ end
23
+
24
+ def shouldTrack?
25
+ !@forced && @variation >= 0
26
+ end
27
+
28
+ def initialize(result, key)
29
+ @experiment = result.experiment
30
+ @variation = result.variation
31
+ @forced = result.forced?
32
+
33
+ @data = {}
34
+ if @experiment && @experiment.data
35
+ var = @variation <0 ? 0 : @variation
36
+ @experiment.data.each do |k, v|
37
+ @data[k] = v[var]
38
+ end
39
+ end
40
+
41
+ @value = @data[key] || nil
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,165 @@
1
+ require 'set'
2
+
3
+ module Growthbook
4
+ class User
5
+ # @returns [String, nil]
6
+ attr_accessor :id
7
+
8
+ # @returns [String, nil]
9
+ attr_accessor :anonId
10
+
11
+ # @returns [Hash, nil]
12
+ attr_reader :attributes
13
+
14
+ # @returns [Array<Growthbook::ExperimentResult>]
15
+ attr_reader :resultsToTrack
16
+
17
+ @client
18
+ @attributeMap = {}
19
+ @experimentsTracked
20
+
21
+ def initialize(anonId, id, attributes, client)
22
+ @anonId = anonId
23
+ @id = id
24
+ @attributes = attributes
25
+ @client = client
26
+ updateAttributeMap
27
+
28
+ @resultsToTrack = []
29
+ @experimentsTracked = Set[]
30
+ end
31
+
32
+ # Set the user attributes
33
+ #
34
+ # @params attributes [Hash, nil] Any user attributes you want to use for experiment targeting
35
+ # Values can be any type, even nested arrays and hashes
36
+ def attributes=(attributes)
37
+ @attributes = attributes
38
+ updateAttributeMap
39
+ end
40
+
41
+ # Run an experiment on this user
42
+ # @param experiment [Growthbook::Experiment, String] If string, will lookup the experiment by id in the client
43
+ # @return [Growthbook::ExperimentResult]
44
+ def experiment(experiment)
45
+ # If experiments are disabled globally
46
+ return getExperimentResult unless @client.enabled
47
+
48
+ # Make sure experiment is always an object (or nil)
49
+ id = ""
50
+ if experiment.is_a? String
51
+ id = experiment
52
+ experiment = @client.getExperiment(id)
53
+ else
54
+ id = experiment.id
55
+ override = @client.getExperiment(id)
56
+ experiment = override if override
57
+ end
58
+
59
+ # No experiment found
60
+ return getExperimentResult unless experiment
61
+
62
+ # User missing required user id type
63
+ userId = experiment.anon ? @anonId : @id
64
+ if !userId
65
+ return getExperimentResult(experiment)
66
+ end
67
+
68
+ # Experiment has targeting rules, check if user passes
69
+ if experiment.targeting
70
+ return getExperimentResult(experiment) unless isTargeted(experiment.targeting)
71
+ end
72
+
73
+ # Experiment has a specific variation forced
74
+ if experiment.force != nil
75
+ return getExperimentResult(experiment, experiment.force, true)
76
+ end
77
+
78
+ # Choose a variation for the user
79
+ variation = Growthbook::Util.chooseVariation(userId, experiment)
80
+ result = getExperimentResult(experiment, variation)
81
+
82
+ # Add to the list of experiments that should be tracked in analytics
83
+ if result.shouldTrack? && !@experimentsTracked.include?(experiment.id)
84
+ @experimentsTracked << experiment.id
85
+ @resultsToTrack << result
86
+ end
87
+
88
+ return result
89
+ end
90
+
91
+ # Run the first matching experiment that defines variation data for the requested key
92
+ # @param key [String, Symbol] The key to look up
93
+ # @return [Growthbook::LookupResult, nil] If nil, no matching experiments found
94
+ def lookupByDataKey(key)
95
+ @client.experiments.each do |exp|
96
+ if exp.data && exp.data.key?(key)
97
+ ret = experiment(exp)
98
+ if ret.variation >= 0
99
+ return Growthbook::LookupResult.new(ret, key)
100
+ end
101
+ end
102
+ end
103
+
104
+ return nil
105
+ end
106
+
107
+ private
108
+
109
+ def getExperimentResult(experiment = nil, variation = -1, forced = false)
110
+ Growthbook::ExperimentResult.new(self, experiment, variation, forced)
111
+ end
112
+
113
+ def flattenUserValues(prefix, val)
114
+ if val.nil?
115
+ return []
116
+ end
117
+
118
+ if val.is_a? Hash
119
+ ret = []
120
+ val.each do |k, v|
121
+ ret.concat(flattenUserValues(prefix.length>0 ? prefix.to_s + "." + k.to_s : k.to_s, v))
122
+ end
123
+ return ret
124
+ end
125
+
126
+ if val.is_a? Array
127
+ val = val.join ","
128
+ elsif !!val == val
129
+ val = val ? "true" : "false"
130
+ end
131
+
132
+ return [
133
+ {
134
+ "k" => prefix.to_s,
135
+ "v" => val.to_s
136
+ }
137
+ ]
138
+ end
139
+
140
+ def updateAttributeMap
141
+ @attributeMap = {}
142
+ flat = flattenUserValues("", @attributes)
143
+ flat.each do |item|
144
+ @attributeMap[item["k"]] = item["v"]
145
+ end
146
+ end
147
+
148
+ def isTargeted(rules)
149
+ pass = true
150
+ rules.each do |rule|
151
+ parts = rule.split(" ", 3)
152
+ if parts.length == 3
153
+ key = parts[0].strip
154
+ actual = @attributeMap[key] || ""
155
+ if !Growthbook::Util.checkRule(actual, parts[1].strip, parts[2].strip)
156
+ pass = false
157
+ break
158
+ end
159
+ end
160
+ end
161
+
162
+ return pass
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,51 @@
1
+ require "fnv"
2
+
3
+ module Growthbook
4
+ class Util
5
+ def self.checkRule(actual, op, desired)
6
+ # Check if both strings are numeric so we can do natural ordering
7
+ # for greater than / less than operators
8
+ numeric = (Float(actual) != nil && Float(desired) != nil) rescue false
9
+
10
+ case op
11
+ when "="
12
+ numeric ? Float(actual) == Float(desired) : actual == desired
13
+ when "!="
14
+ numeric ? Float(actual) != Float(desired) : actual != desired
15
+ when ">"
16
+ numeric ? Float(actual) > Float(desired) : actual > desired
17
+ when "<"
18
+ numeric ? Float(actual) < Float(desired) : actual < desired
19
+ when "~"
20
+ !!(actual =~ Regexp.new(desired)) rescue false
21
+ when "!~"
22
+ !(actual =~ Regexp.new(desired)) rescue false
23
+ else
24
+ true
25
+ end
26
+ end
27
+
28
+ def self.chooseVariation(userId, experiment)
29
+ testId = experiment.id
30
+ weights = experiment.getScaledWeights()
31
+
32
+ # Hash the user id and testName to a number from 0 to 1
33
+ n = (FNV.new.fnv1a_32(userId + testId)%1000)/1000.0
34
+
35
+ cumulativeWeight = 0
36
+
37
+ match = -1
38
+ i = 0
39
+ weights.each do |weight|
40
+ cumulativeWeight += weight
41
+ if n < cumulativeWeight
42
+ match = i
43
+ break
44
+ end
45
+ i+=1
46
+ end
47
+
48
+ return match
49
+ end
50
+ end
51
+ end
metadata ADDED
@@ -0,0 +1,77 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: growthbook
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - GrowthBook
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-12-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.2'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: fnv
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.2.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.2.0
41
+ description: Official GrowthBook SDK for Ruby
42
+ email: jeremy@growthbook.io
43
+ executables: []
44
+ extensions: []
45
+ extra_rdoc_files: []
46
+ files:
47
+ - lib/growthbook.rb
48
+ - lib/growthbook/client.rb
49
+ - lib/growthbook/experiment.rb
50
+ - lib/growthbook/experiment_result.rb
51
+ - lib/growthbook/lookup_result.rb
52
+ - lib/growthbook/user.rb
53
+ - lib/growthbook/util.rb
54
+ homepage: https://github.com/growthbook/growthbook-ruby
55
+ licenses:
56
+ - MIT
57
+ metadata: {}
58
+ post_install_message:
59
+ rdoc_options: []
60
+ require_paths:
61
+ - lib
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ required_rubygems_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ requirements: []
73
+ rubygems_version: 3.1.2
74
+ signing_key:
75
+ specification_version: 4
76
+ summary: GrowthBook SDK for Ruby
77
+ test_files: []