growthbook 0.3.0 → 1.0.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 +13 -13
- data/lib/growthbook/context.rb +115 -39
- data/lib/growthbook/feature.rb +4 -3
- data/lib/growthbook/feature_result.rb +3 -2
- data/lib/growthbook/feature_rule.rb +92 -32
- data/lib/growthbook/inline_experiment.rb +71 -48
- data/lib/growthbook/inline_experiment_result.rb +57 -38
- data/lib/growthbook/util.rb +29 -42
- data/lib/growthbook.rb +1 -5
- metadata +34 -22
- data/lib/growthbook/client.rb +0 -67
- data/lib/growthbook/experiment.rb +0 -72
- data/lib/growthbook/experiment_result.rb +0 -43
- data/lib/growthbook/lookup_result.rb +0 -44
- data/lib/growthbook/user.rb +0 -165
- data/spec/cases.json +0 -2923
- data/spec/client_spec.rb +0 -57
- data/spec/context_spec.rb +0 -124
- data/spec/json_spec.rb +0 -160
- data/spec/user_spec.rb +0 -213
- data/spec/util_spec.rb +0 -154
@@ -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
|
-
# @
|
6
|
+
# @return [String] The globally unique identifier for the experiment
|
6
7
|
attr_accessor :key
|
7
8
|
|
8
|
-
# @
|
9
|
+
# @return [Array<String, Integer, Hash>] The different variations to choose between
|
9
10
|
attr_accessor :variations
|
10
11
|
|
11
|
-
# @
|
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
|
-
# @
|
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
|
-
# @
|
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
|
-
# @
|
27
|
+
# @return [String, nil] Adds the experiment to a namespace
|
27
28
|
attr_accessor :namespace
|
28
29
|
|
29
|
-
# @
|
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
|
-
#
|
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
|
-
|
60
|
-
|
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
|
-
|
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)
|
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['
|
75
|
-
res['
|
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
|
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
|
14
|
-
# @return [Any]
|
12
|
+
# @return [Any] The array value of the assigned variation
|
15
13
|
attr_reader :value
|
16
14
|
|
17
|
-
# If
|
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
|
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
|
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
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
@
|
45
|
-
@
|
46
|
-
@
|
47
|
-
@
|
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
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
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
|
data/lib/growthbook/util.rb
CHANGED
@@ -1,10 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'fnv'
|
4
|
+
require 'bigdecimal'
|
5
|
+
require 'bigdecimal/util'
|
4
6
|
|
5
7
|
module Growthbook
|
8
|
+
# internal use only
|
6
9
|
class Util
|
7
|
-
def self.
|
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,61 +42,41 @@ module Growthbook
|
|
39
42
|
end
|
40
43
|
end
|
41
44
|
|
42
|
-
def self.
|
43
|
-
|
44
|
-
|
45
|
+
def self.hash(seed:, value:, version:)
|
46
|
+
return (FNV.new.fnv1a_32(value + seed) % 1000) / 1000.0 if version == 1
|
47
|
+
return (FNV.new.fnv1a_32(FNV.new.fnv1a_32(seed + value).to_s) % 10_000) / 10_000.0 if version == 2
|
45
48
|
|
46
|
-
|
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
|
63
|
-
end
|
64
|
-
|
65
|
-
def self.hash(str)
|
66
|
-
(FNV.new.fnv1a_32(str) % 1000) / 1000.0
|
49
|
+
-1
|
67
50
|
end
|
68
51
|
|
69
|
-
def self.in_namespace(
|
70
|
-
n = hash("
|
52
|
+
def self.in_namespace(hash_value, namespace)
|
53
|
+
n = hash(seed: "__#{namespace[0]}", value: hash_value, version: 1)
|
71
54
|
n >= namespace[1] && n < namespace[2]
|
72
55
|
end
|
73
56
|
|
74
|
-
def self.get_equal_weights(
|
75
|
-
return [] if
|
57
|
+
def self.get_equal_weights(num_variations)
|
58
|
+
return [] if num_variations < 1
|
76
59
|
|
77
60
|
weights = []
|
78
|
-
(1..
|
79
|
-
weights << (1.0 /
|
61
|
+
(1..num_variations).each do |_i|
|
62
|
+
weights << (1.0 / num_variations)
|
80
63
|
end
|
81
64
|
weights
|
82
65
|
end
|
83
66
|
|
84
67
|
# Determine bucket ranges for experiment variations
|
85
|
-
def self.get_bucket_ranges(
|
68
|
+
def self.get_bucket_ranges(num_variations, coverage = 1, weights = [])
|
86
69
|
# Make sure coverage is within bounds
|
87
70
|
coverage = 1 if coverage.nil?
|
88
71
|
coverage = 0 if coverage.negative?
|
89
72
|
coverage = 1 if coverage > 1
|
90
73
|
|
91
74
|
# Default to equal weights
|
92
|
-
weights = get_equal_weights(
|
75
|
+
weights = get_equal_weights(num_variations) if !weights || weights.length != num_variations
|
93
76
|
|
94
77
|
# If weights don't add up to 1 (or close to it), default to equal weights
|
95
78
|
total = weights.sum
|
96
|
-
weights = get_equal_weights(
|
79
|
+
weights = get_equal_weights(num_variations) if total < 0.99 || total > 1.01
|
97
80
|
|
98
81
|
# Convert weights to ranges
|
99
82
|
cumulative = 0
|
@@ -101,23 +84,23 @@ module Growthbook
|
|
101
84
|
weights.each do |w|
|
102
85
|
start = cumulative
|
103
86
|
cumulative += w
|
104
|
-
ranges << [start, start + coverage * w]
|
87
|
+
ranges << [start, start + (coverage * w)]
|
105
88
|
end
|
106
89
|
|
107
90
|
ranges
|
108
91
|
end
|
109
92
|
|
110
93
|
# Chose a variation based on a hash and range
|
111
|
-
def self.choose_variation(
|
94
|
+
def self.choose_variation(num, ranges)
|
112
95
|
ranges.each_with_index do |range, i|
|
113
|
-
return i if
|
96
|
+
return i if num >= range[0] && num < range[1]
|
114
97
|
end
|
115
98
|
-1
|
116
99
|
end
|
117
100
|
|
118
101
|
# Get an override variation from a url querystring
|
119
102
|
# e.g. http://localhost?my-test=1 will return `1` for id `my-test`
|
120
|
-
def self.get_query_string_override(id, url,
|
103
|
+
def self.get_query_string_override(id, url, num_variations)
|
121
104
|
# Skip if url is empty
|
122
105
|
return nil if url == ''
|
123
106
|
|
@@ -132,7 +115,7 @@ module Growthbook
|
|
132
115
|
return nil unless vals
|
133
116
|
|
134
117
|
val = vals.last
|
135
|
-
return
|
118
|
+
return nil unless val
|
136
119
|
|
137
120
|
# Parse the value as an integer
|
138
121
|
n = begin
|
@@ -144,9 +127,13 @@ module Growthbook
|
|
144
127
|
# Make sure the integer is within range
|
145
128
|
return nil if n.nil?
|
146
129
|
return nil if n.negative?
|
147
|
-
return nil if n >=
|
130
|
+
return nil if n >= num_variations
|
148
131
|
|
149
132
|
n
|
150
133
|
end
|
134
|
+
|
135
|
+
def self.in_range?(num, range)
|
136
|
+
num >= range[0] && num < range[1]
|
137
|
+
end
|
151
138
|
end
|
152
139
|
end
|
data/lib/growthbook.rb
CHANGED
@@ -1,18 +1,14 @@
|
|
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'
|
11
9
|
require 'growthbook/feature'
|
12
10
|
require 'growthbook/feature_result'
|
13
11
|
require 'growthbook/feature_rule'
|
14
12
|
require 'growthbook/inline_experiment'
|
15
13
|
require 'growthbook/inline_experiment_result'
|
16
|
-
require 'growthbook/lookup_result'
|
17
|
-
require 'growthbook/user'
|
18
14
|
require 'growthbook/util'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: growthbook
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- GrowthBook
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-04-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rspec
|
@@ -24,6 +24,34 @@ dependencies:
|
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '3.2'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: simplecov
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.21'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.21'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: simplecov-shields-badge
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.1.0
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.1.0
|
27
55
|
- !ruby/object:Gem::Dependency
|
28
56
|
name: fnv
|
29
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -45,29 +73,19 @@ extensions: []
|
|
45
73
|
extra_rdoc_files: []
|
46
74
|
files:
|
47
75
|
- lib/growthbook.rb
|
48
|
-
- lib/growthbook/client.rb
|
49
76
|
- lib/growthbook/conditions.rb
|
50
77
|
- lib/growthbook/context.rb
|
51
|
-
- lib/growthbook/experiment.rb
|
52
|
-
- lib/growthbook/experiment_result.rb
|
53
78
|
- lib/growthbook/feature.rb
|
54
79
|
- lib/growthbook/feature_result.rb
|
55
80
|
- lib/growthbook/feature_rule.rb
|
56
81
|
- lib/growthbook/inline_experiment.rb
|
57
82
|
- lib/growthbook/inline_experiment_result.rb
|
58
|
-
- lib/growthbook/lookup_result.rb
|
59
|
-
- lib/growthbook/user.rb
|
60
83
|
- lib/growthbook/util.rb
|
61
|
-
- spec/cases.json
|
62
|
-
- spec/client_spec.rb
|
63
|
-
- spec/context_spec.rb
|
64
|
-
- spec/json_spec.rb
|
65
|
-
- spec/user_spec.rb
|
66
|
-
- spec/util_spec.rb
|
67
84
|
homepage: https://github.com/growthbook/growthbook-ruby
|
68
85
|
licenses:
|
69
86
|
- MIT
|
70
|
-
metadata:
|
87
|
+
metadata:
|
88
|
+
rubygems_mfa_required: 'true'
|
71
89
|
post_install_message:
|
72
90
|
rdoc_options: []
|
73
91
|
require_paths:
|
@@ -76,7 +94,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
76
94
|
requirements:
|
77
95
|
- - ">="
|
78
96
|
- !ruby/object:Gem::Version
|
79
|
-
version:
|
97
|
+
version: 2.5.0
|
80
98
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
81
99
|
requirements:
|
82
100
|
- - ">="
|
@@ -87,10 +105,4 @@ rubygems_version: 3.1.2
|
|
87
105
|
signing_key:
|
88
106
|
specification_version: 4
|
89
107
|
summary: GrowthBook SDK for Ruby
|
90
|
-
test_files:
|
91
|
-
- spec/json_spec.rb
|
92
|
-
- spec/user_spec.rb
|
93
|
-
- spec/util_spec.rb
|
94
|
-
- spec/context_spec.rb
|
95
|
-
- spec/client_spec.rb
|
96
|
-
- spec/cases.json
|
108
|
+
test_files: []
|
data/lib/growthbook/client.rb
DELETED
@@ -1,67 +0,0 @@
|
|
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
|
@@ -1,72 +0,0 @@
|
|
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
|