split 3.4.0 → 4.0.1
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/.github/FUNDING.yml +1 -0
- data/.github/dependabot.yml +7 -0
- data/.github/workflows/ci.yml +71 -0
- data/.rubocop.yml +177 -1
- data/.rubocop_todo.yml +40 -493
- data/CHANGELOG.md +41 -0
- data/Gemfile +1 -0
- data/README.md +26 -6
- data/Rakefile +1 -0
- data/gemfiles/{4.2.gemfile → 6.1.gemfile} +1 -1
- data/gemfiles/7.0.gemfile +9 -0
- data/lib/split/algorithms/block_randomization.rb +1 -0
- data/lib/split/algorithms/weighted_sample.rb +2 -1
- data/lib/split/algorithms/whiplash.rb +3 -2
- data/lib/split/alternative.rb +1 -0
- data/lib/split/cache.rb +28 -0
- data/lib/split/combined_experiments_helper.rb +1 -0
- data/lib/split/configuration.rb +8 -12
- data/lib/split/dashboard/helpers.rb +1 -0
- data/lib/split/dashboard/pagination_helpers.rb +1 -0
- data/lib/split/dashboard/paginator.rb +1 -0
- data/lib/split/dashboard/public/dashboard.js +10 -0
- data/lib/split/dashboard/public/style.css +5 -0
- data/lib/split/dashboard/views/_controls.erb +13 -0
- data/lib/split/dashboard.rb +17 -2
- data/lib/split/encapsulated_helper.rb +3 -2
- data/lib/split/engine.rb +5 -4
- data/lib/split/exceptions.rb +1 -0
- data/lib/split/experiment.rb +82 -60
- data/lib/split/experiment_catalog.rb +1 -3
- data/lib/split/extensions/string.rb +1 -0
- data/lib/split/goals_collection.rb +1 -0
- data/lib/split/helper.rb +26 -7
- data/lib/split/metric.rb +2 -1
- data/lib/split/persistence/cookie_adapter.rb +6 -1
- data/lib/split/persistence/redis_adapter.rb +5 -0
- data/lib/split/persistence/session_adapter.rb +1 -0
- data/lib/split/persistence.rb +4 -2
- data/lib/split/redis_interface.rb +8 -28
- data/lib/split/trial.rb +20 -10
- data/lib/split/user.rb +15 -3
- data/lib/split/version.rb +2 -4
- data/lib/split/zscore.rb +1 -0
- data/lib/split.rb +9 -3
- data/spec/alternative_spec.rb +1 -1
- data/spec/cache_spec.rb +88 -0
- data/spec/configuration_spec.rb +17 -15
- data/spec/dashboard_spec.rb +45 -5
- data/spec/encapsulated_helper_spec.rb +1 -1
- data/spec/experiment_spec.rb +78 -13
- data/spec/goals_collection_spec.rb +1 -1
- data/spec/helper_spec.rb +68 -32
- data/spec/persistence/cookie_adapter_spec.rb +1 -1
- data/spec/persistence/redis_adapter_spec.rb +9 -0
- data/spec/redis_interface_spec.rb +0 -69
- data/spec/spec_helper.rb +5 -6
- data/spec/trial_spec.rb +45 -19
- data/spec/user_spec.rb +34 -3
- data/split.gemspec +7 -7
- metadata +27 -35
- data/.travis.yml +0 -60
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,44 @@
|
|
1
|
+
## 4.0.1 (December 30th, 2021)
|
2
|
+
|
3
|
+
Bugfixes:
|
4
|
+
- ab_test must return metadata on error or if split is disabled/excluded user (@andrehjr, #622)
|
5
|
+
- Fix versioned experiments when used with allow_multiple_experiments=control (@andrehjr, #613)
|
6
|
+
- Only block Pinterest bot (@huoxito, #606)
|
7
|
+
- Respect experiment defaults when loading experiments in initializer. (@mattwd7, #599)
|
8
|
+
- Removes metadata key when it updated to nil (@andrehjr, #633)
|
9
|
+
- Force experiment does not count for metrics (@andrehjr, #637)
|
10
|
+
- Fix cleanup_old_versions! misbehaviour (@serggl, #661)
|
11
|
+
|
12
|
+
Features:
|
13
|
+
- Make goals accessible via on_trial_complete callbacks (@robin-phung, #625)
|
14
|
+
- Replace usage of SimpleRandom with RubyStats(Used for Beta Distribution RNG) (@andrehjr, #616)
|
15
|
+
- Introduce enable/disable experiment cohorting (@robin-phung, #615)
|
16
|
+
- Add on_experiment_winner_choose callback (@GenaMinenkov, #574)
|
17
|
+
- Add Split::Cache to reduce load on Redis (@rdh, #648)
|
18
|
+
- Caching based optimization in the experiment#save path (@amangup, #652)
|
19
|
+
- Adds config option for cookie domain (@joedelia, #664)
|
20
|
+
|
21
|
+
Misc:
|
22
|
+
- Drop support for Ruby < 2.5 (@andrehjr, #627)
|
23
|
+
- Drop support for Rails < 5 (@andrehjr, #607)
|
24
|
+
- Bump minimum required redis to 4.2 (@andrehjr, #628)
|
25
|
+
- Removed repeated loading from config (@robin-phung, #619)
|
26
|
+
- Simplify RedisInterface usage when persisting Experiment alternatives (@andrehjr, #632)
|
27
|
+
- Remove redis_url impl. Deprecated on version 2.2 (@andrehjr, #631)
|
28
|
+
- Remove thread_safe config as redis-rb is thread_safe by default (@andrehjr, #630)
|
29
|
+
- Fix typo of in `Split::Trial` class variable (TomasBarry, #644)
|
30
|
+
- Single HSET to update values, instead of multiple ones (@andrehjr, #640)
|
31
|
+
- Use Redis#hmset to keep compatibility with Redis < 4.0 (@andrehjr, #659)
|
32
|
+
- Remove 'set' parsing for alternatives. Sets were used as storage and deprecated on 0.x (@andrehjr, #639)
|
33
|
+
- Adding documentation related to what is stored on cookies. (@andrehjr, #634)
|
34
|
+
- Keep railtie defined under the Split gem namespace (@avit, #666)
|
35
|
+
- Update RSpec helper to support block syntax (@clowder, #665)
|
36
|
+
|
37
|
+
## 3.4.1 (November 12th, 2019)
|
38
|
+
|
39
|
+
Bugfixes:
|
40
|
+
- Reference ActionController directly when including split helpers, to avoid breaking Rails API Controllers (@andrehjr, #602)
|
41
|
+
|
1
42
|
## 3.4.0 (November 9th, 2019)
|
2
43
|
|
3
44
|
Features:
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# [Split](https://libraries.io/rubygems/split)
|
2
2
|
|
3
3
|
[](http://badge.fury.io/rb/split)
|
4
|
-
|
4
|
+

|
5
5
|
[](https://codeclimate.com/github/splitrb/split)
|
6
6
|
[](https://codeclimate.com/github/splitrb/split/coverage)
|
7
7
|
[](https://github.com/RichardLitt/standard-readme)
|
@@ -175,8 +175,10 @@ module SplitHelper
|
|
175
175
|
# use_ab_test(signup_form: "single_page", pricing: "show_enterprise_prices")
|
176
176
|
#
|
177
177
|
def use_ab_test(alternatives_by_experiment)
|
178
|
-
allow_any_instance_of(Split::Helper).to receive(:ab_test) do |_receiver, experiment|
|
179
|
-
alternatives_by_experiment.fetch(experiment) { |key| raise "Unknown experiment '#{key}'" }
|
178
|
+
allow_any_instance_of(Split::Helper).to receive(:ab_test) do |_receiver, experiment, &block|
|
179
|
+
variant = alternatives_by_experiment.fetch(experiment) { |key| raise "Unknown experiment '#{key}'" }
|
180
|
+
block.call(variant) unless block.nil?
|
181
|
+
variant
|
180
182
|
end
|
181
183
|
end
|
182
184
|
end
|
@@ -263,7 +265,7 @@ Split.configure do |config|
|
|
263
265
|
end
|
264
266
|
```
|
265
267
|
|
266
|
-
|
268
|
+
When using the cookie persistence, Split stores data into an anonymous tracking cookie named 'split', which expires in 1 year. To change that, set the `persistence_cookie_length` in the configuration (unit of time in seconds).
|
267
269
|
|
268
270
|
```ruby
|
269
271
|
Split.configure do |config|
|
@@ -272,6 +274,8 @@ Split.configure do |config|
|
|
272
274
|
end
|
273
275
|
```
|
274
276
|
|
277
|
+
The data stored consists of the experiment name and the variants the user is in. Example: { "experiment_name" => "variant_a" }
|
278
|
+
|
275
279
|
__Note:__ Using cookies depends on `ActionDispatch::Cookies` or any identical API
|
276
280
|
|
277
281
|
#### Redis
|
@@ -386,6 +390,8 @@ Split.configure do |config|
|
|
386
390
|
# before experiment reset or deleted
|
387
391
|
config.on_before_experiment_reset = -> (example) { # Do something on reset }
|
388
392
|
config.on_before_experiment_delete = -> (experiment) { # Do something else on delete }
|
393
|
+
# after experiment winner had been set
|
394
|
+
config.on_experiment_winner_choose = -> (experiment) { # Do something on winner choose }
|
389
395
|
end
|
390
396
|
```
|
391
397
|
|
@@ -644,7 +650,7 @@ The API to define goals for an experiment is this:
|
|
644
650
|
ab_test({link_color: ["purchase", "refund"]}, "red", "blue")
|
645
651
|
```
|
646
652
|
|
647
|
-
or you can
|
653
|
+
or you can define them in a configuration file:
|
648
654
|
|
649
655
|
```ruby
|
650
656
|
Split.configure do |config|
|
@@ -752,6 +758,20 @@ split_config = YAML.load_file(Rails.root.join('config', 'split.yml'))
|
|
752
758
|
Split.redis = split_config[Rails.env]
|
753
759
|
```
|
754
760
|
|
761
|
+
### Redis Caching (v4.0+)
|
762
|
+
|
763
|
+
In some high-volume usage scenarios, Redis load can be incurred by repeated
|
764
|
+
fetches for fairly static data. Enabling caching will reduce this load.
|
765
|
+
|
766
|
+
```ruby
|
767
|
+
Split.configuration.cache = true
|
768
|
+
````
|
769
|
+
|
770
|
+
This currently caches:
|
771
|
+
- `Split::ExperimentCatalog.find`
|
772
|
+
- `Split::Experiment.start_time`
|
773
|
+
- `Split::Experiment.winner`
|
774
|
+
|
755
775
|
## Namespaces
|
756
776
|
|
757
777
|
If you're running multiple, separate instances of Split you may want
|
@@ -768,7 +788,7 @@ library. To configure Split to use `Redis::Namespace`, do the following:
|
|
768
788
|
```
|
769
789
|
|
770
790
|
2. Configure `Split.redis` to use a `Redis::Namespace` instance (possible in an
|
771
|
-
|
791
|
+
initializer):
|
772
792
|
|
773
793
|
```ruby
|
774
794
|
redis = Redis.new(url: ENV['REDIS_URL']) # or whatever config you want
|
data/Rakefile
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module Split
|
3
4
|
module Algorithms
|
4
5
|
module WeightedSample
|
@@ -8,7 +9,7 @@ module Split
|
|
8
9
|
total = weights.inject(:+)
|
9
10
|
point = rand * total
|
10
11
|
|
11
|
-
experiment.alternatives.zip(weights).each do |n,w|
|
12
|
+
experiment.alternatives.zip(weights).each do |n, w|
|
12
13
|
return n if w >= point
|
13
14
|
point -= w
|
14
15
|
end
|
@@ -1,7 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
# A multi-armed bandit implementation inspired by
|
3
4
|
# @aaronsw and victorykit/whiplash
|
4
|
-
require '
|
5
|
+
require 'rubystats'
|
5
6
|
|
6
7
|
module Split
|
7
8
|
module Algorithms
|
@@ -16,7 +17,7 @@ module Split
|
|
16
17
|
def arm_guess(participants, completions)
|
17
18
|
a = [participants, 0].max
|
18
19
|
b = [participants-completions, 0].max
|
19
|
-
|
20
|
+
Rubystats::BetaDistribution.new(a+fairness_constant, b+fairness_constant).rng
|
20
21
|
end
|
21
22
|
|
22
23
|
def best_guess(alternatives)
|
data/lib/split/alternative.rb
CHANGED
data/lib/split/cache.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Split
|
4
|
+
class Cache
|
5
|
+
|
6
|
+
def self.clear
|
7
|
+
@cache = nil
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.fetch(namespace, key)
|
11
|
+
return yield unless Split.configuration.cache
|
12
|
+
|
13
|
+
@cache ||= {}
|
14
|
+
@cache[namespace] ||= {}
|
15
|
+
|
16
|
+
value = @cache[namespace][key]
|
17
|
+
return value if value
|
18
|
+
|
19
|
+
@cache[namespace][key] = yield
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.clear_key(key)
|
23
|
+
@cache&.keys&.each do |namespace|
|
24
|
+
@cache[namespace]&.delete(key)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/split/configuration.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module Split
|
3
4
|
class Configuration
|
4
5
|
attr_accessor :ignore_ip_addresses
|
@@ -10,6 +11,7 @@ module Split
|
|
10
11
|
attr_accessor :enabled
|
11
12
|
attr_accessor :persistence
|
12
13
|
attr_accessor :persistence_cookie_length
|
14
|
+
attr_accessor :persistence_cookie_domain
|
13
15
|
attr_accessor :algorithm
|
14
16
|
attr_accessor :store_override
|
15
17
|
attr_accessor :start_manually
|
@@ -20,12 +22,14 @@ module Split
|
|
20
22
|
attr_accessor :on_experiment_reset
|
21
23
|
attr_accessor :on_experiment_delete
|
22
24
|
attr_accessor :on_before_experiment_reset
|
25
|
+
attr_accessor :on_experiment_winner_choose
|
23
26
|
attr_accessor :on_before_experiment_delete
|
24
27
|
attr_accessor :include_rails_helper
|
25
28
|
attr_accessor :beta_probability_simulations
|
26
29
|
attr_accessor :winning_alternative_recalculation_interval
|
27
30
|
attr_accessor :redis
|
28
31
|
attr_accessor :dashboard_pagination_default_per_page
|
32
|
+
attr_accessor :cache
|
29
33
|
|
30
34
|
attr_reader :experiments
|
31
35
|
|
@@ -84,7 +88,7 @@ module Split
|
|
84
88
|
'LinkedInBot' => 'LinkedIn bot',
|
85
89
|
'LongURL' => 'URL expander service',
|
86
90
|
'NING' => 'NING - Yet Another Twitter Swarmer',
|
87
|
-
'
|
91
|
+
'Pinterestbot' => 'Pinterest Bot',
|
88
92
|
'redditbot' => 'Reddit Bot',
|
89
93
|
'ShortLinkTranslate' => 'Link shortener',
|
90
94
|
'Slackbot' => 'Slackbot link expander',
|
@@ -173,7 +177,7 @@ module Split
|
|
173
177
|
end
|
174
178
|
|
175
179
|
def normalize_alternatives(alternatives)
|
176
|
-
given_probability, num_with_probability = alternatives.inject([0,0]) do |a,v|
|
180
|
+
given_probability, num_with_probability = alternatives.inject([0, 0]) do |a, v|
|
177
181
|
p, n = a
|
178
182
|
if percent = value_for(v, :percent)
|
179
183
|
[p + percent, n + 1]
|
@@ -216,12 +220,14 @@ module Split
|
|
216
220
|
@on_experiment_delete = proc{|experiment|}
|
217
221
|
@on_before_experiment_reset = proc{|experiment|}
|
218
222
|
@on_before_experiment_delete = proc{|experiment|}
|
223
|
+
@on_experiment_winner_choose = proc{|experiment|}
|
219
224
|
@db_failover_allow_parameter_override = false
|
220
225
|
@allow_multiple_experiments = false
|
221
226
|
@enabled = true
|
222
227
|
@experiments = {}
|
223
228
|
@persistence = Split::Persistence::SessionAdapter
|
224
229
|
@persistence_cookie_length = 31536000 # One year from now
|
230
|
+
@persistence_cookie_domain = nil
|
225
231
|
@algorithm = Split::Algorithms::WeightedSample
|
226
232
|
@include_rails_helper = true
|
227
233
|
@beta_probability_simulations = 10000
|
@@ -230,16 +236,6 @@ module Split
|
|
230
236
|
@dashboard_pagination_default_per_page = 10
|
231
237
|
end
|
232
238
|
|
233
|
-
def redis_url=(value)
|
234
|
-
warn '[DEPRECATED] `redis_url=` is deprecated in favor of `redis=`'
|
235
|
-
self.redis = value
|
236
|
-
end
|
237
|
-
|
238
|
-
def redis_url
|
239
|
-
warn '[DEPRECATED] `redis_url` is deprecated in favor of `redis`'
|
240
|
-
self.redis
|
241
|
-
end
|
242
|
-
|
243
239
|
private
|
244
240
|
|
245
241
|
def value_for(hash, key)
|
@@ -22,3 +22,13 @@ function confirmReopen() {
|
|
22
22
|
var agree = confirm("This will reopen the experiment. Are you sure?");
|
23
23
|
return agree ? true : false;
|
24
24
|
}
|
25
|
+
|
26
|
+
function confirmEnableCohorting(){
|
27
|
+
var agree = confirm("This will enable the cohorting of the experiment. Are you sure?");
|
28
|
+
return agree ? true : false;
|
29
|
+
}
|
30
|
+
|
31
|
+
function confirmDisableCohorting(){
|
32
|
+
var agree = confirm("This will disable the cohorting of the experiment. Note: Existing participants will continue to receive their alternative and may continue to convert. Are you sure?");
|
33
|
+
return agree ? true : false;
|
34
|
+
}
|
@@ -2,7 +2,20 @@
|
|
2
2
|
<form action="<%= url "/reopen?experiment=#{experiment.name}" %>" method='post' onclick="return confirmReopen()">
|
3
3
|
<input type="submit" value="Reopen Experiment">
|
4
4
|
</form>
|
5
|
+
<% else %>
|
6
|
+
<% if experiment.cohorting_disabled? %>
|
7
|
+
<form action="<%= url "/update_cohorting?experiment=#{experiment.name}" %>" method='post' onclick="return confirmEnableCohorting()">
|
8
|
+
<input type="hidden" name="cohorting_action" value="enable">
|
9
|
+
<input type="submit" value="Enable Cohorting" class="green">
|
10
|
+
</form>
|
11
|
+
<% else %>
|
12
|
+
<form action="<%= url "/update_cohorting?experiment=#{experiment.name}" %>" method='post' onclick="return confirmDisableCohorting()">
|
13
|
+
<input type="hidden" name="cohorting_action" value="disable">
|
14
|
+
<input type="submit" value="Disable Cohorting" class="red">
|
15
|
+
</form>
|
16
|
+
<% end %>
|
5
17
|
<% end %>
|
18
|
+
<span class="divider">|</span>
|
6
19
|
<% if experiment.start_time %>
|
7
20
|
<form action="<%= url "/reset?experiment=#{experiment.name}" %>" method='post' onclick="return confirmReset()">
|
8
21
|
<input type="submit" value="Reset Data">
|
data/lib/split/dashboard.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'sinatra/base'
|
3
4
|
require 'split'
|
4
5
|
require 'bigdecimal'
|
@@ -35,8 +36,11 @@ module Split
|
|
35
36
|
post '/force_alternative' do
|
36
37
|
experiment = Split::ExperimentCatalog.find(params[:experiment])
|
37
38
|
alternative = Split::Alternative.new(params[:alternative], experiment.name)
|
38
|
-
|
39
|
-
|
39
|
+
|
40
|
+
cookies = JSON.parse(request.cookies['split_override']) rescue {}
|
41
|
+
cookies[experiment.name] = alternative.name
|
42
|
+
response.set_cookie('split_override', { value: cookies.to_json, path: '/' })
|
43
|
+
|
40
44
|
redirect url('/')
|
41
45
|
end
|
42
46
|
|
@@ -65,6 +69,17 @@ module Split
|
|
65
69
|
redirect url('/')
|
66
70
|
end
|
67
71
|
|
72
|
+
post '/update_cohorting' do
|
73
|
+
@experiment = Split::ExperimentCatalog.find(params[:experiment])
|
74
|
+
case params[:cohorting_action].downcase
|
75
|
+
when "enable"
|
76
|
+
@experiment.enable_cohorting
|
77
|
+
when "disable"
|
78
|
+
@experiment.disable_cohorting
|
79
|
+
end
|
80
|
+
redirect url('/')
|
81
|
+
end
|
82
|
+
|
68
83
|
delete '/experiment' do
|
69
84
|
@experiment = Split::ExperimentCatalog.find(params[:experiment])
|
70
85
|
@experiment.delete
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require "split/helper"
|
3
4
|
|
4
5
|
# Split's helper exposes all kinds of methods we don't want to
|
@@ -28,8 +29,8 @@ module Split
|
|
28
29
|
end
|
29
30
|
end
|
30
31
|
|
31
|
-
def ab_test(*arguments
|
32
|
-
split_context_shim.ab_test(*arguments
|
32
|
+
def ab_test(*arguments, &block)
|
33
|
+
split_context_shim.ab_test(*arguments, &block)
|
33
34
|
end
|
34
35
|
|
35
36
|
private
|
data/lib/split/engine.rb
CHANGED
@@ -1,13 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module Split
|
3
4
|
class Engine < ::Rails::Engine
|
4
5
|
initializer "split" do |app|
|
5
6
|
if Split.configuration.include_rails_helper
|
6
7
|
ActiveSupport.on_load(:action_controller) do
|
7
|
-
include Split::Helper
|
8
|
-
helper Split::Helper
|
9
|
-
include Split::CombinedExperimentsHelper
|
10
|
-
helper Split::CombinedExperimentsHelper
|
8
|
+
::ActionController::Base.send :include, Split::Helper
|
9
|
+
::ActionController::Base.helper Split::Helper
|
10
|
+
::ActionController::Base.send :include, Split::CombinedExperimentsHelper
|
11
|
+
::ActionController::Base.helper Split::CombinedExperimentsHelper
|
11
12
|
end
|
12
13
|
end
|
13
14
|
end
|