growthbook 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/growthbook/conditions.rb +174 -0
- data/lib/growthbook/context.rb +225 -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 +50 -0
- data/lib/growthbook/util.rb +116 -15
- data/lib/growthbook.rb +11 -2
- data/spec/cases.json +2768 -0
- data/spec/client_spec.rb +57 -0
- data/spec/context_spec.rb +120 -0
- data/spec/json_spec.rb +159 -0
- data/spec/user_spec.rb +213 -0
- data/spec/util_spec.rb +154 -0
- metadata +22 -3
@@ -0,0 +1,50 @@
|
|
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
|
+
# The attribute used to split traffic
|
18
|
+
# @return [String]
|
19
|
+
attr_reader :hash_attribute
|
20
|
+
|
21
|
+
# The value of the hashAttribute
|
22
|
+
# @return [String]
|
23
|
+
attr_reader :hash_value
|
24
|
+
|
25
|
+
def initialize(
|
26
|
+
in_experiment,
|
27
|
+
variation_id,
|
28
|
+
value,
|
29
|
+
hash_attribute,
|
30
|
+
hash_value
|
31
|
+
)
|
32
|
+
|
33
|
+
@in_experiment = in_experiment
|
34
|
+
@variation_id = variation_id
|
35
|
+
@value = value
|
36
|
+
@hash_attribute = hash_attribute
|
37
|
+
@hash_value = hash_value
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_json(*_args)
|
41
|
+
res = {}
|
42
|
+
res['inExperiment'] = @in_experiment
|
43
|
+
res['variationId'] = @variation_id
|
44
|
+
res['value'] = @value
|
45
|
+
res['hashAttribute'] = @hash_attribute
|
46
|
+
res['hashValue'] = @hash_value
|
47
|
+
res
|
48
|
+
end
|
49
|
+
end
|
50
|
+
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'
|