growthbook 0.0.1 → 0.3.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 +4 -4
- data/lib/growthbook/conditions.rb +174 -0
- data/lib/growthbook/context.rb +233 -0
- data/lib/growthbook/feature.rb +41 -0
- data/lib/growthbook/feature_result.rb +61 -0
- data/lib/growthbook/feature_rule.rb +78 -0
- data/lib/growthbook/inline_experiment.rb +80 -0
- data/lib/growthbook/inline_experiment_result.rb +62 -0
- data/lib/growthbook/util.rb +116 -15
- data/lib/growthbook.rb +11 -2
- data/spec/cases.json +2923 -0
- data/spec/client_spec.rb +57 -0
- data/spec/context_spec.rb +124 -0
- data/spec/json_spec.rb +160 -0
- data/spec/user_spec.rb +213 -0
- data/spec/util_spec.rb +154 -0
- metadata +22 -3
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Growthbook
|
4
|
+
class InlineExperimentResult
|
5
|
+
# Whether or not the user is in the experiment
|
6
|
+
# @return [Bool]
|
7
|
+
attr_reader :in_experiment
|
8
|
+
|
9
|
+
# The array index of the assigned variation
|
10
|
+
# @return [Integer]
|
11
|
+
attr_reader :variation_id
|
12
|
+
|
13
|
+
# The assigned variation value
|
14
|
+
# @return [Any]
|
15
|
+
attr_reader :value
|
16
|
+
|
17
|
+
# If the variation was randomly assigned based on user attribute hashes
|
18
|
+
# @return [Bool]
|
19
|
+
attr_reader :hash_used
|
20
|
+
|
21
|
+
# The attribute used to split traffic
|
22
|
+
# @return [String]
|
23
|
+
attr_reader :hash_attribute
|
24
|
+
|
25
|
+
# The value of the hashAttribute
|
26
|
+
# @return [String]
|
27
|
+
attr_reader :hash_value
|
28
|
+
|
29
|
+
attr_reader :feature_id
|
30
|
+
|
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
|
48
|
+
end
|
49
|
+
|
50
|
+
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
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
data/lib/growthbook/util.rb
CHANGED
@@ -1,25 +1,39 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'fnv'
|
2
4
|
|
3
5
|
module Growthbook
|
4
6
|
class Util
|
5
7
|
def self.checkRule(actual, op, desired)
|
6
8
|
# Check if both strings are numeric so we can do natural ordering
|
7
9
|
# for greater than / less than operators
|
8
|
-
numeric =
|
10
|
+
numeric = begin
|
11
|
+
(!Float(actual).nil? && !Float(desired).nil?)
|
12
|
+
rescue StandardError
|
13
|
+
false
|
14
|
+
end
|
9
15
|
|
10
16
|
case op
|
11
|
-
when
|
17
|
+
when '='
|
12
18
|
numeric ? Float(actual) == Float(desired) : actual == desired
|
13
|
-
when
|
19
|
+
when '!='
|
14
20
|
numeric ? Float(actual) != Float(desired) : actual != desired
|
15
|
-
when
|
21
|
+
when '>'
|
16
22
|
numeric ? Float(actual) > Float(desired) : actual > desired
|
17
|
-
when
|
23
|
+
when '<'
|
18
24
|
numeric ? Float(actual) < Float(desired) : actual < desired
|
19
|
-
when
|
20
|
-
|
21
|
-
|
22
|
-
|
25
|
+
when '~'
|
26
|
+
begin
|
27
|
+
!!(actual =~ Regexp.new(desired))
|
28
|
+
rescue StandardError
|
29
|
+
false
|
30
|
+
end
|
31
|
+
when '!~'
|
32
|
+
begin
|
33
|
+
actual !~ Regexp.new(desired)
|
34
|
+
rescue StandardError
|
35
|
+
false
|
36
|
+
end
|
23
37
|
else
|
24
38
|
true
|
25
39
|
end
|
@@ -27,10 +41,10 @@ module Growthbook
|
|
27
41
|
|
28
42
|
def self.chooseVariation(userId, experiment)
|
29
43
|
testId = experiment.id
|
30
|
-
weights = experiment.getScaledWeights
|
44
|
+
weights = experiment.getScaledWeights
|
31
45
|
|
32
46
|
# Hash the user id and testName to a number from 0 to 1
|
33
|
-
n = (FNV.new.fnv1a_32(userId + testId)%1000)/1000.0
|
47
|
+
n = (FNV.new.fnv1a_32(userId + testId) % 1000) / 1000.0
|
34
48
|
|
35
49
|
cumulativeWeight = 0
|
36
50
|
|
@@ -42,10 +56,97 @@ module Growthbook
|
|
42
56
|
match = i
|
43
57
|
break
|
44
58
|
end
|
45
|
-
i+=1
|
59
|
+
i += 1
|
60
|
+
end
|
61
|
+
|
62
|
+
match
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.hash(str)
|
66
|
+
(FNV.new.fnv1a_32(str) % 1000) / 1000.0
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.in_namespace(userId, namespace)
|
70
|
+
n = hash("#{userId}__#{namespace[0]}")
|
71
|
+
n >= namespace[1] && n < namespace[2]
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.get_equal_weights(numVariations)
|
75
|
+
return [] if numVariations < 1
|
76
|
+
|
77
|
+
weights = []
|
78
|
+
(1..numVariations).each do |_i|
|
79
|
+
weights << (1.0 / numVariations)
|
80
|
+
end
|
81
|
+
weights
|
82
|
+
end
|
83
|
+
|
84
|
+
# Determine bucket ranges for experiment variations
|
85
|
+
def self.get_bucket_ranges(numVariations, coverage = 1, weights = [])
|
86
|
+
# Make sure coverage is within bounds
|
87
|
+
coverage = 1 if coverage.nil?
|
88
|
+
coverage = 0 if coverage.negative?
|
89
|
+
coverage = 1 if coverage > 1
|
90
|
+
|
91
|
+
# Default to equal weights
|
92
|
+
weights = get_equal_weights(numVariations) if !weights || weights.length != numVariations
|
93
|
+
|
94
|
+
# If weights don't add up to 1 (or close to it), default to equal weights
|
95
|
+
total = weights.sum
|
96
|
+
weights = get_equal_weights(numVariations) if total < 0.99 || total > 1.01
|
97
|
+
|
98
|
+
# Convert weights to ranges
|
99
|
+
cumulative = 0
|
100
|
+
ranges = []
|
101
|
+
weights.each do |w|
|
102
|
+
start = cumulative
|
103
|
+
cumulative += w
|
104
|
+
ranges << [start, start + coverage * w]
|
105
|
+
end
|
106
|
+
|
107
|
+
ranges
|
108
|
+
end
|
109
|
+
|
110
|
+
# Chose a variation based on a hash and range
|
111
|
+
def self.choose_variation(n, ranges)
|
112
|
+
ranges.each_with_index do |range, i|
|
113
|
+
return i if n >= range[0] && n < range[1]
|
114
|
+
end
|
115
|
+
-1
|
116
|
+
end
|
117
|
+
|
118
|
+
# Get an override variation from a url querystring
|
119
|
+
# e.g. http://localhost?my-test=1 will return `1` for id `my-test`
|
120
|
+
def self.get_query_string_override(id, url, numVariations)
|
121
|
+
# Skip if url is empty
|
122
|
+
return nil if url == ''
|
123
|
+
|
124
|
+
# Parse out the query string
|
125
|
+
parsed = URI(url)
|
126
|
+
return nil unless parsed.query
|
127
|
+
|
128
|
+
qs = URI.decode_www_form(parsed.query)
|
129
|
+
|
130
|
+
# Look for `id` in the querystring and get the value
|
131
|
+
vals = qs.assoc(id)
|
132
|
+
return nil unless vals
|
133
|
+
|
134
|
+
val = vals.last
|
135
|
+
return nill unless val
|
136
|
+
|
137
|
+
# Parse the value as an integer
|
138
|
+
n = begin
|
139
|
+
Integer(val)
|
140
|
+
rescue StandardError
|
141
|
+
nil
|
46
142
|
end
|
47
143
|
|
48
|
-
|
144
|
+
# Make sure the integer is within range
|
145
|
+
return nil if n.nil?
|
146
|
+
return nil if n.negative?
|
147
|
+
return nil if n >= numVariations
|
148
|
+
|
149
|
+
n
|
49
150
|
end
|
50
151
|
end
|
51
|
-
end
|
152
|
+
end
|
data/lib/growthbook.rb
CHANGED
@@ -1,9 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Growthbook
|
2
4
|
end
|
3
5
|
|
4
6
|
require 'growthbook/client'
|
7
|
+
require 'growthbook/conditions'
|
8
|
+
require 'growthbook/context'
|
9
|
+
require 'growthbook/experiment'
|
5
10
|
require 'growthbook/experiment_result'
|
11
|
+
require 'growthbook/feature'
|
12
|
+
require 'growthbook/feature_result'
|
13
|
+
require 'growthbook/feature_rule'
|
14
|
+
require 'growthbook/inline_experiment'
|
15
|
+
require 'growthbook/inline_experiment_result'
|
6
16
|
require 'growthbook/lookup_result'
|
7
|
-
require 'growthbook/
|
17
|
+
require 'growthbook/user'
|
8
18
|
require 'growthbook/util'
|
9
|
-
require 'growthbook/user'
|