growthbook 0.3.0 → 1.0.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 +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
|