split 3.4.1 → 4.0.0.pre
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/.rubocop.yml +177 -1
- data/.rubocop_todo.yml +40 -493
- data/.travis.yml +14 -42
- data/CHANGELOG.md +35 -0
- data/Gemfile +1 -0
- data/README.md +19 -1
- data/Rakefile +1 -0
- data/lib/split.rb +8 -2
- 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 +6 -12
- data/lib/split/dashboard.rb +17 -2
- 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/encapsulated_helper.rb +3 -2
- data/lib/split/engine.rb +1 -0
- data/lib/split/exceptions.rb +1 -0
- data/lib/split/experiment.rb +81 -59
- 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.rb +4 -2
- data/lib/split/persistence/cookie_adapter.rb +1 -0
- data/lib/split/persistence/redis_adapter.rb +5 -0
- data/lib/split/persistence/session_adapter.rb +1 -0
- data/lib/split/redis_interface.rb +8 -28
- data/lib/split/trial.rb +20 -10
- data/lib/split/user.rb +14 -2
- data/lib/split/version.rb +2 -4
- data/lib/split/zscore.rb +1 -0
- data/spec/alternative_spec.rb +1 -1
- data/spec/cache_spec.rb +88 -0
- data/spec/configuration_spec.rb +1 -14
- data/spec/dashboard_spec.rb +45 -5
- data/spec/encapsulated_helper_spec.rb +1 -1
- data/spec/experiment_spec.rb +78 -7
- 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 +17 -0
- data/split.gemspec +7 -7
- metadata +23 -34
- data/gemfiles/4.2.gemfile +0 -9
data/.travis.yml
CHANGED
@@ -1,60 +1,32 @@
|
|
1
1
|
language: ruby
|
2
2
|
rvm:
|
3
|
-
- 1.9.3
|
4
|
-
- 2.0
|
5
|
-
- 2.1.10
|
6
|
-
- 2.2.2
|
7
|
-
- 2.3.8
|
8
|
-
- 2.4.9
|
9
3
|
- 2.5.7
|
10
|
-
- 2.6.
|
4
|
+
- 2.6.6
|
5
|
+
- 2.7.1
|
6
|
+
|
7
|
+
services:
|
8
|
+
- redis-server
|
11
9
|
|
12
10
|
gemfile:
|
13
|
-
- gemfiles/4.2.gemfile
|
14
11
|
- gemfiles/5.0.gemfile
|
15
12
|
- gemfiles/5.1.gemfile
|
16
13
|
- gemfiles/5.2.gemfile
|
17
14
|
- gemfiles/6.0.gemfile
|
18
15
|
|
19
|
-
matrix:
|
20
|
-
exclude:
|
21
|
-
- rvm: 1.9.3
|
22
|
-
gemfile: gemfiles/5.0.gemfile
|
23
|
-
- rvm: 1.9.3
|
24
|
-
gemfile: gemfiles/5.1.gemfile
|
25
|
-
- rvm: 1.9.3
|
26
|
-
gemfile: gemfiles/5.2.gemfile
|
27
|
-
- rvm: 1.9.3
|
28
|
-
gemfile: gemfiles/6.0.gemfile
|
29
|
-
- rvm: 2.0
|
30
|
-
gemfile: gemfiles/5.0.gemfile
|
31
|
-
- rvm: 2.0
|
32
|
-
gemfile: gemfiles/5.1.gemfile
|
33
|
-
- rvm: 2.0
|
34
|
-
gemfile: gemfiles/5.2.gemfile
|
35
|
-
- rvm: 2.0
|
36
|
-
gemfile: gemfiles/6.0.gemfile
|
37
|
-
- rvm: 2.1.10
|
38
|
-
gemfile: gemfiles/5.0.gemfile
|
39
|
-
- rvm: 2.1.10
|
40
|
-
gemfile: gemfiles/5.1.gemfile
|
41
|
-
- rvm: 2.1.10
|
42
|
-
gemfile: gemfiles/5.2.gemfile
|
43
|
-
- rvm: 2.1.10
|
44
|
-
gemfile: gemfiles/6.0.gemfile
|
45
|
-
- rvm: 2.2.2
|
46
|
-
gemfile: gemfiles/6.0.gemfile
|
47
|
-
- rvm: 2.3.8
|
48
|
-
gemfile: gemfiles/6.0.gemfile
|
49
|
-
- rvm: 2.4.9
|
50
|
-
gemfile: gemfiles/6.0.gemfile
|
51
|
-
|
52
16
|
before_install:
|
53
17
|
- gem uninstall -v '>= 2' -i $(rvm gemdir)@global -ax bundler || true
|
54
18
|
- gem install bundler --version=1.17.3
|
55
19
|
|
20
|
+
before_script:
|
21
|
+
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
|
22
|
+
- chmod +x ./cc-test-reporter
|
23
|
+
- ./cc-test-reporter before-build
|
24
|
+
|
56
25
|
script:
|
57
|
-
- RAILS_ENV=test bundle exec rake spec
|
26
|
+
- RAILS_ENV=test bundle exec rake spec
|
27
|
+
|
28
|
+
after_script:
|
29
|
+
- ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
|
58
30
|
|
59
31
|
cache: bundler
|
60
32
|
sudo: false
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,38 @@
|
|
1
|
+
## 4.0.0.pre
|
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
|
+
|
10
|
+
Features:
|
11
|
+
- Make goals accessible via on_trial_complete callbacks (@robin-phung, #625)
|
12
|
+
- Replace usage of SimpleRandom with RubyStats(Used for Beta Distribution RNG) (@andrehjr, #616)
|
13
|
+
- Introduce enable/disable experiment cohorting (@robin-phung, #615)
|
14
|
+
- Add on_experiment_winner_choose callback (@GenaMinenkov, #574)
|
15
|
+
- Add Split::Cache to reduce load on Redis (@rdh, #648)
|
16
|
+
- Caching based optimization in the experiment#save path (@amangup, #652)
|
17
|
+
|
18
|
+
Misc:
|
19
|
+
- Drop support for Ruby < 2.5 (@andrehjr, #627)
|
20
|
+
- Drop support for Rails < 5 (@andrehkr, #607)
|
21
|
+
- Bump minimum required redis to 4.2 (@andrehjr, #628)
|
22
|
+
- Removed repeated loading from config (@robin-phung, #619)
|
23
|
+
- Simplify RedisInterface usage when persisting Experiment alternatives (@andrehjr, #632)
|
24
|
+
- Remove redis_url impl. Deprecated on version 2.2 (@andrehjr, #631)
|
25
|
+
- Remove thread_safe config as redis-rb is thread_safe by default (@andrehjr, #630)
|
26
|
+
- Fix typo of in `Split::Trial` class variable (TomasBarry, #644)
|
27
|
+
- Single HSET to update values, instead of multiple ones (@andrehjr, #640)
|
28
|
+
- Remove 'set' parsing for alternatives. Sets were used as storage and deprecated on 0.x (@andrehjr, #639)
|
29
|
+
- Adding documentation related to what is stored on cookies. (@andrehjr, #634)
|
30
|
+
|
31
|
+
## 3.4.1 (November 12th, 2019)
|
32
|
+
|
33
|
+
Bugfixes:
|
34
|
+
- Reference ActionController directly when including split helpers, to avoid breaking Rails API Controllers (@andrehjr, #602)
|
35
|
+
|
1
36
|
## 3.4.0 (November 9th, 2019)
|
2
37
|
|
3
38
|
Features:
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -263,7 +263,7 @@ Split.configure do |config|
|
|
263
263
|
end
|
264
264
|
```
|
265
265
|
|
266
|
-
|
266
|
+
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
267
|
|
268
268
|
```ruby
|
269
269
|
Split.configure do |config|
|
@@ -272,6 +272,8 @@ Split.configure do |config|
|
|
272
272
|
end
|
273
273
|
```
|
274
274
|
|
275
|
+
The data stored consists of the experiment name and the variants the user is in. Example: { "experiment_name" => "variant_a" }
|
276
|
+
|
275
277
|
__Note:__ Using cookies depends on `ActionDispatch::Cookies` or any identical API
|
276
278
|
|
277
279
|
#### Redis
|
@@ -386,6 +388,8 @@ Split.configure do |config|
|
|
386
388
|
# before experiment reset or deleted
|
387
389
|
config.on_before_experiment_reset = -> (example) { # Do something on reset }
|
388
390
|
config.on_before_experiment_delete = -> (experiment) { # Do something else on delete }
|
391
|
+
# after experiment winner had been set
|
392
|
+
config.on_experiment_winner_choose = -> (experiment) { # Do something on winner choose }
|
389
393
|
end
|
390
394
|
```
|
391
395
|
|
@@ -752,6 +756,20 @@ split_config = YAML.load_file(Rails.root.join('config', 'split.yml'))
|
|
752
756
|
Split.redis = split_config[Rails.env]
|
753
757
|
```
|
754
758
|
|
759
|
+
### Redis Caching (v4.0+)
|
760
|
+
|
761
|
+
In some high-volume usage scenarios, Redis load can be incurred by repeated
|
762
|
+
fetches for fairly static data. Enabling caching will reduce this load.
|
763
|
+
|
764
|
+
```ruby
|
765
|
+
Split.configuration.cache = true
|
766
|
+
````
|
767
|
+
|
768
|
+
This currently caches:
|
769
|
+
- `Split::ExperimentCatalog.find`
|
770
|
+
- `Split::Experiment.start_time`
|
771
|
+
- `Split::Experiment.winner`
|
772
|
+
|
755
773
|
## Namespaces
|
756
774
|
|
757
775
|
If you're running multiple, separate instances of Split you may want
|
data/Rakefile
CHANGED
data/lib/split.rb
CHANGED
@@ -1,10 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'redis'
|
3
4
|
|
4
5
|
require 'split/algorithms/block_randomization'
|
5
6
|
require 'split/algorithms/weighted_sample'
|
6
7
|
require 'split/algorithms/whiplash'
|
7
8
|
require 'split/alternative'
|
9
|
+
require 'split/cache'
|
8
10
|
require 'split/configuration'
|
9
11
|
require 'split/encapsulated_helper'
|
10
12
|
require 'split/exceptions'
|
@@ -35,9 +37,9 @@ module Split
|
|
35
37
|
# `Redis::DistRedis`, or `Redis::Namespace`.
|
36
38
|
def redis=(server)
|
37
39
|
@redis = if server.is_a?(String)
|
38
|
-
Redis.new(:
|
40
|
+
Redis.new(url: server)
|
39
41
|
elsif server.is_a?(Hash)
|
40
|
-
Redis.new(server
|
42
|
+
Redis.new(server)
|
41
43
|
elsif server.respond_to?(:smembers)
|
42
44
|
server
|
43
45
|
else
|
@@ -64,6 +66,10 @@ module Split
|
|
64
66
|
self.configuration ||= Configuration.new
|
65
67
|
yield(configuration)
|
66
68
|
end
|
69
|
+
|
70
|
+
def cache(namespace, key, &block)
|
71
|
+
Split::Cache.fetch(namespace, key, &block)
|
72
|
+
end
|
67
73
|
end
|
68
74
|
|
69
75
|
# Check to see if being run in a Rails application. If so, wait until before_initialize to run configuration so Gems that create ENV variables have the chance to initialize first.
|
@@ -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
|
@@ -20,12 +21,14 @@ module Split
|
|
20
21
|
attr_accessor :on_experiment_reset
|
21
22
|
attr_accessor :on_experiment_delete
|
22
23
|
attr_accessor :on_before_experiment_reset
|
24
|
+
attr_accessor :on_experiment_winner_choose
|
23
25
|
attr_accessor :on_before_experiment_delete
|
24
26
|
attr_accessor :include_rails_helper
|
25
27
|
attr_accessor :beta_probability_simulations
|
26
28
|
attr_accessor :winning_alternative_recalculation_interval
|
27
29
|
attr_accessor :redis
|
28
30
|
attr_accessor :dashboard_pagination_default_per_page
|
31
|
+
attr_accessor :cache
|
29
32
|
|
30
33
|
attr_reader :experiments
|
31
34
|
|
@@ -84,7 +87,7 @@ module Split
|
|
84
87
|
'LinkedInBot' => 'LinkedIn bot',
|
85
88
|
'LongURL' => 'URL expander service',
|
86
89
|
'NING' => 'NING - Yet Another Twitter Swarmer',
|
87
|
-
'
|
90
|
+
'Pinterestbot' => 'Pinterest Bot',
|
88
91
|
'redditbot' => 'Reddit Bot',
|
89
92
|
'ShortLinkTranslate' => 'Link shortener',
|
90
93
|
'Slackbot' => 'Slackbot link expander',
|
@@ -173,7 +176,7 @@ module Split
|
|
173
176
|
end
|
174
177
|
|
175
178
|
def normalize_alternatives(alternatives)
|
176
|
-
given_probability, num_with_probability = alternatives.inject([0,0]) do |a,v|
|
179
|
+
given_probability, num_with_probability = alternatives.inject([0, 0]) do |a, v|
|
177
180
|
p, n = a
|
178
181
|
if percent = value_for(v, :percent)
|
179
182
|
[p + percent, n + 1]
|
@@ -216,6 +219,7 @@ module Split
|
|
216
219
|
@on_experiment_delete = proc{|experiment|}
|
217
220
|
@on_before_experiment_reset = proc{|experiment|}
|
218
221
|
@on_before_experiment_delete = proc{|experiment|}
|
222
|
+
@on_experiment_winner_choose = proc{|experiment|}
|
219
223
|
@db_failover_allow_parameter_override = false
|
220
224
|
@allow_multiple_experiments = false
|
221
225
|
@enabled = true
|
@@ -230,16 +234,6 @@ module Split
|
|
230
234
|
@dashboard_pagination_default_per_page = 10
|
231
235
|
end
|
232
236
|
|
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
237
|
private
|
244
238
|
|
245
239
|
def value_for(hash, key)
|
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
|
@@ -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">
|