split 2.1.0 → 2.2.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/.codeclimate.yml +30 -0
- data/.csslintrc +2 -0
- data/.eslintignore +1 -0
- data/.eslintrc +213 -0
- data/.rubocop.yml +1156 -0
- data/.travis.yml +4 -0
- data/Appraisals +5 -0
- data/CHANGELOG.md +23 -1
- data/Gemfile +1 -0
- data/README.md +43 -21
- data/gemfiles/4.1.gemfile +1 -0
- data/gemfiles/4.2.gemfile +1 -0
- data/gemfiles/5.0.gemfile +10 -0
- data/lib/split.rb +15 -23
- data/lib/split/alternative.rb +0 -1
- data/lib/split/configuration.rb +13 -2
- data/lib/split/dashboard.rb +5 -0
- data/lib/split/dashboard/public/dashboard-filtering.js +3 -3
- data/lib/split/dashboard/views/_experiment.erb +4 -0
- data/lib/split/encapsulated_helper.rb +2 -15
- data/lib/split/experiment.rb +63 -54
- data/lib/split/extensions.rb +1 -1
- data/lib/split/goals_collection.rb +1 -1
- data/lib/split/redis_interface.rb +51 -0
- data/lib/split/user.rb +1 -1
- data/lib/split/version.rb +1 -1
- data/spec/configuration_spec.rb +19 -5
- data/spec/dashboard_spec.rb +15 -0
- data/spec/encapsulated_helper_spec.rb +35 -4
- data/spec/experiment_spec.rb +38 -5
- data/spec/helper_spec.rb +59 -16
- data/spec/persistence/dual_adapter_spec.rb +102 -0
- data/spec/redis_interface_spec.rb +111 -0
- data/spec/spec_helper.rb +11 -10
- data/spec/split_spec.rb +43 -0
- data/split.gemspec +2 -3
- metadata +20 -23
- data/lib/split/extensions/array.rb +0 -5
data/.travis.yml
CHANGED
|
@@ -5,9 +5,13 @@ rvm:
|
|
|
5
5
|
gemfile:
|
|
6
6
|
- gemfiles/4.1.gemfile
|
|
7
7
|
- gemfiles/4.2.gemfile
|
|
8
|
+
- gemfiles/5.0.gemfile
|
|
8
9
|
|
|
9
10
|
before_install:
|
|
10
11
|
- gem install bundler
|
|
11
12
|
|
|
13
|
+
script:
|
|
14
|
+
- RAILS_ENV=test bundle exec rake spec && bundle exec codeclimate-test-reporter
|
|
15
|
+
|
|
12
16
|
cache: bundler
|
|
13
17
|
sudo: false
|
data/Appraisals
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,28 @@
|
|
|
1
|
+
## 2.2.0 (November 11th, 2016)
|
|
2
|
+
|
|
3
|
+
Features:
|
|
4
|
+
|
|
5
|
+
- Remove dependency on Redis::Namespace (@bschaeffer, #425)
|
|
6
|
+
- Make resetting on experiment change optional (@moggyboy, #430)
|
|
7
|
+
- Add ability to force alternative on dashboard (@ccallebs, #437)
|
|
8
|
+
|
|
9
|
+
Bugfixes:
|
|
10
|
+
|
|
11
|
+
- Fix variations reset across page loads for multiple=control and improve coverage (@Vasfed, #432)
|
|
12
|
+
|
|
13
|
+
Misc:
|
|
14
|
+
|
|
15
|
+
- Remove Explicit Return (@BradHudson, #441)
|
|
16
|
+
- Update Redis config docs (@bschaeffer, #422)
|
|
17
|
+
- Harden HTTP Basic snippet against timing attacks (@eliotsykes, #443)
|
|
18
|
+
- Removed a couple old ruby 1.8 hacks (@andrew, #456)
|
|
19
|
+
- Run tests on rails 5 (@andrew, #457)
|
|
20
|
+
- Fixed a few codeclimate warnings (@andrew, #458)
|
|
21
|
+
- Use codeclimate for test coverage (@andrew #455)
|
|
22
|
+
|
|
1
23
|
## 2.1.0 (August 8th, 2016)
|
|
2
24
|
|
|
3
|
-
Features
|
|
25
|
+
Features:
|
|
4
26
|
|
|
5
27
|
- Support REDIS_PROVIDER variable used in Heroku (@kartikluke, #426)
|
|
6
28
|
|
data/Gemfile
CHANGED
data/README.md
CHANGED
|
@@ -8,9 +8,8 @@ Split is designed to be hacker friendly, allowing for maximum customisation and
|
|
|
8
8
|
|
|
9
9
|
[](http://badge.fury.io/rb/split)
|
|
10
10
|
[](http://travis-ci.org/splitrb/split)
|
|
11
|
-
[](https://codeclimate.com/github/splitrb/split)
|
|
12
|
-
[](https://codeclimate.com/github/splitrb/split)
|
|
12
|
+
[](https://codeclimate.com/github/splitrb/split/coverage)
|
|
14
13
|
|
|
15
14
|
|
|
16
15
|
# Backers
|
|
@@ -436,8 +435,22 @@ mount Split::Dashboard, at: 'split'
|
|
|
436
435
|
You may want to password protect that page, you can do so with `Rack::Auth::Basic` (in your split initializer file)
|
|
437
436
|
|
|
438
437
|
```ruby
|
|
438
|
+
# Rails apps or apps that already depend on activesupport
|
|
439
|
+
Split::Dashboard.use Rack::Auth::Basic do |username, password|
|
|
440
|
+
# Protect against timing attacks:
|
|
441
|
+
# - Use & (do not use &&) so that it doesn't short circuit.
|
|
442
|
+
# - Use `variable_size_secure_compare` to stop length information leaking
|
|
443
|
+
ActiveSupport::SecurityUtils.variable_size_secure_compare(username, ENV["SPLIT_USERNAME"]) &
|
|
444
|
+
ActiveSupport::SecurityUtils.variable_size_secure_compare(password, ENV["SPLIT_PASSWORD"])
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
# Apps without activesupport
|
|
439
448
|
Split::Dashboard.use Rack::Auth::Basic do |username, password|
|
|
440
|
-
|
|
449
|
+
# Protect against timing attacks:
|
|
450
|
+
# - Use & (do not use &&) so that it doesn't short circuit.
|
|
451
|
+
# - Use digests to stop length information leaking
|
|
452
|
+
Rack::Utils.secure_compare(::Digest::SHA256.hexdigest(username), ::Digest::SHA256.hexdigest(ENV["SPLIT_USERNAME"])) &
|
|
453
|
+
Rack::Utils.secure_compare(::Digest::SHA256.hexdigest(password), ::Digest::SHA256.hexdigest(ENV["SPLIT_PASSWORD"]))
|
|
441
454
|
end
|
|
442
455
|
```
|
|
443
456
|
|
|
@@ -469,11 +482,16 @@ Split.configure do |config|
|
|
|
469
482
|
config.persistence = Split::Persistence::SessionAdapter
|
|
470
483
|
#config.start_manually = false ## new test will have to be started manually from the admin panel. default false
|
|
471
484
|
config.include_rails_helper = true
|
|
472
|
-
config.
|
|
485
|
+
config.redis = "redis://custom.redis.url:6380"
|
|
473
486
|
end
|
|
474
487
|
```
|
|
475
488
|
|
|
476
|
-
Split looks for the Redis host in the environment variable
|
|
489
|
+
Split looks for the Redis host in the environment variable `REDIS_URL` then
|
|
490
|
+
defaults to `redis://localhost:6379` if not specified by configure block.
|
|
491
|
+
|
|
492
|
+
On platforms like Heroku, Split will use the value of `REDIS_PROVIDER` to
|
|
493
|
+
determine which env variable key to use when retrieving the host config. This
|
|
494
|
+
defaults to `REDIS_URL`.
|
|
477
495
|
|
|
478
496
|
### Filtering
|
|
479
497
|
|
|
@@ -694,7 +712,7 @@ Split has a `redis` setter which can be given a string or a Redis
|
|
|
694
712
|
object. This means if you're already using Redis in your app, Split
|
|
695
713
|
can re-use the existing connection.
|
|
696
714
|
|
|
697
|
-
String: `Split.redis = 'localhost:6379'`
|
|
715
|
+
String: `Split.redis = 'redis://localhost:6379'`
|
|
698
716
|
|
|
699
717
|
Redis: `Split.redis = $redis`
|
|
700
718
|
|
|
@@ -705,11 +723,11 @@ appropriately.
|
|
|
705
723
|
Here's our `config/split.yml`:
|
|
706
724
|
|
|
707
725
|
```yml
|
|
708
|
-
development: localhost:6379
|
|
709
|
-
test: localhost:6379
|
|
710
|
-
staging: redis1.example.com:6379
|
|
711
|
-
fi: localhost:6379
|
|
712
|
-
production: redis1.example.com:6379
|
|
726
|
+
development: redis://localhost:6379
|
|
727
|
+
test: redis://localhost:6379
|
|
728
|
+
staging: redis://redis1.example.com:6379
|
|
729
|
+
fi: redis://localhost:6379
|
|
730
|
+
production: redis://redis1.example.com:6379
|
|
713
731
|
```
|
|
714
732
|
|
|
715
733
|
And our initializer:
|
|
@@ -725,18 +743,22 @@ If you're running multiple, separate instances of Split you may want
|
|
|
725
743
|
to namespace the keyspaces so they do not overlap. This is not unlike
|
|
726
744
|
the approach taken by many memcached clients.
|
|
727
745
|
|
|
728
|
-
This feature
|
|
729
|
-
|
|
730
|
-
in your Redis server.
|
|
746
|
+
This feature can be provided by the [redis-namespace](https://github.com/defunkt/redis-namespace)
|
|
747
|
+
library. To configure Split to use `Redis::Namespace`, do the following:
|
|
731
748
|
|
|
732
|
-
|
|
749
|
+
1. Add `redis-namespace` to your Gemfile:
|
|
733
750
|
|
|
734
|
-
```ruby
|
|
735
|
-
|
|
736
|
-
```
|
|
751
|
+
```ruby
|
|
752
|
+
gem 'redis-namespace'
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
2. Configure `Split.redis` to use a `Redis::Namespace` instance (possible in an
|
|
756
|
+
intializer):
|
|
737
757
|
|
|
738
|
-
|
|
739
|
-
|
|
758
|
+
```ruby
|
|
759
|
+
redis = Redis.new(url: ENV['REDIS_URL']) # or whatever config you want
|
|
760
|
+
Split.redis = Redis::Namespace.new(:your_namespace, redis: redis)
|
|
761
|
+
```
|
|
740
762
|
|
|
741
763
|
## Outside of a Web Session
|
|
742
764
|
|
data/gemfiles/4.1.gemfile
CHANGED
data/gemfiles/4.2.gemfile
CHANGED
data/lib/split.rb
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
metric
|
|
12
12
|
persistence
|
|
13
13
|
encapsulated_helper
|
|
14
|
+
redis_interface
|
|
14
15
|
trial
|
|
15
16
|
user
|
|
16
17
|
version
|
|
@@ -19,36 +20,27 @@
|
|
|
19
20
|
end
|
|
20
21
|
|
|
21
22
|
require 'split/engine' if defined?(Rails)
|
|
22
|
-
require 'redis/namespace'
|
|
23
23
|
|
|
24
24
|
module Split
|
|
25
25
|
extend self
|
|
26
26
|
attr_accessor :configuration
|
|
27
27
|
|
|
28
28
|
# Accepts:
|
|
29
|
-
# 1. A
|
|
30
|
-
# 2.
|
|
31
|
-
# 3.
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
# or `Redis::Namespace`.
|
|
29
|
+
# 1. A redis URL (valid for `Redis.new(url: url)`)
|
|
30
|
+
# 2. an options hash compatible with `Redis.new`
|
|
31
|
+
# 3. or a valid Redis instance (one that responds to `#smembers`). Likely,
|
|
32
|
+
# this will be an instance of either `Redis`, `Redis::Client`,
|
|
33
|
+
# `Redis::DistRedis`, or `Redis::Namespace`.
|
|
35
34
|
def redis=(server)
|
|
36
|
-
if server.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
redis = Redis.new(:host => host, :port => port,
|
|
43
|
-
:thread_safe => true, :db => db)
|
|
44
|
-
end
|
|
45
|
-
namespace ||= :split
|
|
46
|
-
|
|
47
|
-
@redis = Redis::Namespace.new(namespace, :redis => redis)
|
|
48
|
-
elsif server.respond_to? :namespace=
|
|
49
|
-
@redis = server
|
|
35
|
+
@redis = if server.is_a?(String)
|
|
36
|
+
Redis.new(:url => server, :thread_safe => true)
|
|
37
|
+
elsif server.is_a?(Hash)
|
|
38
|
+
Redis.new(server.merge(:thread_safe => true))
|
|
39
|
+
elsif server.respond_to?(:smembers)
|
|
40
|
+
server
|
|
50
41
|
else
|
|
51
|
-
|
|
42
|
+
raise ArgumentError,
|
|
43
|
+
"You must supply a url, options hash or valid Redis connection instance"
|
|
52
44
|
end
|
|
53
45
|
end
|
|
54
46
|
|
|
@@ -56,7 +48,7 @@ module Split
|
|
|
56
48
|
# create a new one.
|
|
57
49
|
def redis
|
|
58
50
|
return @redis if @redis
|
|
59
|
-
self.redis = self.configuration.
|
|
51
|
+
self.redis = self.configuration.redis
|
|
60
52
|
self.redis
|
|
61
53
|
end
|
|
62
54
|
|
data/lib/split/alternative.rb
CHANGED
data/lib/split/configuration.rb
CHANGED
|
@@ -15,6 +15,7 @@ module Split
|
|
|
15
15
|
attr_accessor :algorithm
|
|
16
16
|
attr_accessor :store_override
|
|
17
17
|
attr_accessor :start_manually
|
|
18
|
+
attr_accessor :reset_manually
|
|
18
19
|
attr_accessor :on_trial
|
|
19
20
|
attr_accessor :on_trial_choose
|
|
20
21
|
attr_accessor :on_trial_complete
|
|
@@ -24,7 +25,7 @@ module Split
|
|
|
24
25
|
attr_accessor :on_before_experiment_delete
|
|
25
26
|
attr_accessor :include_rails_helper
|
|
26
27
|
attr_accessor :beta_probability_simulations
|
|
27
|
-
attr_accessor :
|
|
28
|
+
attr_accessor :redis
|
|
28
29
|
|
|
29
30
|
attr_reader :experiments
|
|
30
31
|
|
|
@@ -211,7 +212,17 @@ module Split
|
|
|
211
212
|
@algorithm = Split::Algorithms::WeightedSample
|
|
212
213
|
@include_rails_helper = true
|
|
213
214
|
@beta_probability_simulations = 10000
|
|
214
|
-
@
|
|
215
|
+
@redis = ENV.fetch(ENV.fetch('REDIS_PROVIDER', 'REDIS_URL'), 'redis://localhost:6379')
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def redis_url=(value)
|
|
219
|
+
warn '[DEPRECATED] `redis_url=` is deprecated in favor of `redis=`'
|
|
220
|
+
self.redis = value
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def redis_url
|
|
224
|
+
warn '[DEPRECATED] `redis_url` is deprecated in favor of `redis`'
|
|
225
|
+
self.redis
|
|
215
226
|
end
|
|
216
227
|
|
|
217
228
|
private
|
data/lib/split/dashboard.rb
CHANGED
|
@@ -30,6 +30,11 @@ module Split
|
|
|
30
30
|
erb :index
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
+
post '/force_alternative' do
|
|
34
|
+
Split::User.new(self)[params[:experiment]] = params[:alternative]
|
|
35
|
+
redirect url('/')
|
|
36
|
+
end
|
|
37
|
+
|
|
33
38
|
post '/experiment' do
|
|
34
39
|
@experiment = Split::ExperimentCatalog.find(params[:experiment])
|
|
35
40
|
@alternative = Split::Alternative.new(params[:alternative], params[:experiment])
|
|
@@ -2,7 +2,7 @@ $(function() {
|
|
|
2
2
|
$('#filter').on('keyup', function() {
|
|
3
3
|
$input = $(this);
|
|
4
4
|
|
|
5
|
-
if ($input.val()
|
|
5
|
+
if ($input.val() === '') {
|
|
6
6
|
$('div.experiment').show();
|
|
7
7
|
return false;
|
|
8
8
|
}
|
|
@@ -21,7 +21,7 @@ $(function() {
|
|
|
21
21
|
|
|
22
22
|
$('#toggle-active').on('click', function() {
|
|
23
23
|
$button = $(this);
|
|
24
|
-
if ($button.val()
|
|
24
|
+
if ($button.val() === 'Hide active') {
|
|
25
25
|
$button.val('Show active');
|
|
26
26
|
} else {
|
|
27
27
|
$button.val('Hide active');
|
|
@@ -32,7 +32,7 @@ $(function() {
|
|
|
32
32
|
|
|
33
33
|
$('#toggle-completed').on('click', function() {
|
|
34
34
|
$button = $(this);
|
|
35
|
-
if ($button.val()
|
|
35
|
+
if ($button.val() === 'Hide completed') {
|
|
36
36
|
$button.val('Show completed');
|
|
37
37
|
} else {
|
|
38
38
|
$button.val('Hide completed');
|
|
@@ -51,6 +51,10 @@
|
|
|
51
51
|
<% if alternative.control? %>
|
|
52
52
|
<em>control</em>
|
|
53
53
|
<% end %>
|
|
54
|
+
<form action="<%= url('force_alternative') + '?experiment=' + experiment.name %>" method='post'>
|
|
55
|
+
<input type='hidden' name='alternative' value='<%= h alternative.name %>'>
|
|
56
|
+
<input type="submit" value="Force for current user" class="green">
|
|
57
|
+
</form>
|
|
54
58
|
</td>
|
|
55
59
|
<td><%= alternative.participant_count %></td>
|
|
56
60
|
<td><%= alternative.unfinished_count %></td>
|
|
@@ -26,21 +26,8 @@ module Split
|
|
|
26
26
|
end
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
def ab_test(*arguments)
|
|
30
|
-
|
|
31
|
-
# TODO there must be a better way to pass a block straight
|
|
32
|
-
# through to the original ab_test
|
|
33
|
-
if block_given?
|
|
34
|
-
if defined?(capture) # a block in a rails view
|
|
35
|
-
block = Proc.new { yield(ret) }
|
|
36
|
-
concat(capture(ret, &block))
|
|
37
|
-
false
|
|
38
|
-
else
|
|
39
|
-
yield(ret)
|
|
40
|
-
end
|
|
41
|
-
else
|
|
42
|
-
ret
|
|
43
|
-
end
|
|
29
|
+
def ab_test(*arguments,&block)
|
|
30
|
+
split_context_shim.ab_test(*arguments,&block)
|
|
44
31
|
end
|
|
45
32
|
|
|
46
33
|
private
|
data/lib/split/experiment.rb
CHANGED
|
@@ -80,29 +80,15 @@ module Split
|
|
|
80
80
|
validate!
|
|
81
81
|
|
|
82
82
|
if new_record?
|
|
83
|
-
Split.redis.sadd(:experiments, name)
|
|
84
83
|
start unless Split.configuration.start_manually
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
save_metadata
|
|
88
|
-
else
|
|
89
|
-
existing_alternatives = load_alternatives_from_redis
|
|
90
|
-
existing_goals = Split::GoalsCollection.new(@name).load_from_redis
|
|
91
|
-
existing_metadata = load_metadata_from_redis
|
|
92
|
-
unless existing_alternatives == @alternatives.map(&:name) && existing_goals == @goals && existing_metadata == @metadata
|
|
93
|
-
reset
|
|
94
|
-
@alternatives.each(&:delete)
|
|
95
|
-
goals_collection.delete
|
|
96
|
-
delete_metadata
|
|
97
|
-
Split.redis.del(@name)
|
|
98
|
-
@alternatives.reverse.each {|a| Split.redis.lpush(name, a.name)}
|
|
99
|
-
goals_collection.save
|
|
100
|
-
save_metadata
|
|
101
|
-
end
|
|
84
|
+
elsif experiment_configuration_has_changed?
|
|
85
|
+
reset unless Split.configuration.reset_manually
|
|
102
86
|
end
|
|
103
87
|
|
|
104
|
-
|
|
105
|
-
|
|
88
|
+
persist_experiment_configuration if new_record? || experiment_configuration_has_changed?
|
|
89
|
+
|
|
90
|
+
redis.hset(experiment_config_key, :resettable, resettable)
|
|
91
|
+
redis.hset(experiment_config_key, :algorithm, algorithm.to_s)
|
|
106
92
|
self
|
|
107
93
|
end
|
|
108
94
|
|
|
@@ -115,7 +101,7 @@ module Split
|
|
|
115
101
|
end
|
|
116
102
|
|
|
117
103
|
def new_record?
|
|
118
|
-
!
|
|
104
|
+
!redis.exists(name)
|
|
119
105
|
end
|
|
120
106
|
|
|
121
107
|
def ==(obj)
|
|
@@ -149,8 +135,9 @@ module Split
|
|
|
149
135
|
end
|
|
150
136
|
|
|
151
137
|
def winner
|
|
152
|
-
|
|
153
|
-
|
|
138
|
+
experiment_winner = redis.hget(:experiment_winner, name)
|
|
139
|
+
if experiment_winner
|
|
140
|
+
Split::Alternative.new(experiment_winner, name)
|
|
154
141
|
else
|
|
155
142
|
nil
|
|
156
143
|
end
|
|
@@ -161,7 +148,7 @@ module Split
|
|
|
161
148
|
end
|
|
162
149
|
|
|
163
150
|
def winner=(winner_name)
|
|
164
|
-
|
|
151
|
+
redis.hset(:experiment_winner, name, winner_name.to_s)
|
|
165
152
|
end
|
|
166
153
|
|
|
167
154
|
def participant_count
|
|
@@ -173,21 +160,21 @@ module Split
|
|
|
173
160
|
end
|
|
174
161
|
|
|
175
162
|
def reset_winner
|
|
176
|
-
|
|
163
|
+
redis.hdel(:experiment_winner, name)
|
|
177
164
|
end
|
|
178
165
|
|
|
179
166
|
def start
|
|
180
|
-
|
|
167
|
+
redis.hset(:experiment_start_times, @name, Time.now.to_i)
|
|
181
168
|
end
|
|
182
169
|
|
|
183
170
|
def start_time
|
|
184
|
-
t =
|
|
171
|
+
t = redis.hget(:experiment_start_times, @name)
|
|
185
172
|
if t
|
|
186
173
|
# Check if stored time is an integer
|
|
187
174
|
if t =~ /^[-+]?[0-9]+$/
|
|
188
|
-
|
|
175
|
+
Time.at(t.to_i)
|
|
189
176
|
else
|
|
190
|
-
|
|
177
|
+
Time.parse(t)
|
|
191
178
|
end
|
|
192
179
|
end
|
|
193
180
|
end
|
|
@@ -205,11 +192,11 @@ module Split
|
|
|
205
192
|
end
|
|
206
193
|
|
|
207
194
|
def version
|
|
208
|
-
@version ||= (
|
|
195
|
+
@version ||= (redis.get("#{name}:version").to_i || 0)
|
|
209
196
|
end
|
|
210
197
|
|
|
211
198
|
def increment_version
|
|
212
|
-
@version =
|
|
199
|
+
@version = redis.incr("#{name}:version")
|
|
213
200
|
end
|
|
214
201
|
|
|
215
202
|
def key
|
|
@@ -247,24 +234,21 @@ module Split
|
|
|
247
234
|
def delete
|
|
248
235
|
Split.configuration.on_before_experiment_delete.call(self)
|
|
249
236
|
if Split.configuration.start_manually
|
|
250
|
-
|
|
237
|
+
redis.hdel(:experiment_start_times, @name)
|
|
251
238
|
end
|
|
252
|
-
alternatives.each(&:delete)
|
|
253
239
|
reset_winner
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
goals_collection.delete
|
|
257
|
-
delete_metadata
|
|
240
|
+
redis.srem(:experiments, name)
|
|
241
|
+
remove_experiment_configuration
|
|
258
242
|
Split.configuration.on_experiment_delete.call(self)
|
|
259
243
|
increment_version
|
|
260
244
|
end
|
|
261
245
|
|
|
262
246
|
def delete_metadata
|
|
263
|
-
|
|
247
|
+
redis.del(metadata_key)
|
|
264
248
|
end
|
|
265
249
|
|
|
266
250
|
def load_from_redis
|
|
267
|
-
exp_config =
|
|
251
|
+
exp_config = redis.hgetall(experiment_config_key)
|
|
268
252
|
|
|
269
253
|
options = {
|
|
270
254
|
resettable: exp_config['resettable'],
|
|
@@ -297,8 +281,6 @@ module Split
|
|
|
297
281
|
end
|
|
298
282
|
|
|
299
283
|
def estimate_winning_alternative(goal = nil)
|
|
300
|
-
# TODO - refactor out functionality to work with and without goals
|
|
301
|
-
|
|
302
284
|
# initialize a hash of beta distributions based on the alternatives' conversion rates
|
|
303
285
|
beta_params = calc_beta_params(goal)
|
|
304
286
|
|
|
@@ -395,11 +377,11 @@ module Split
|
|
|
395
377
|
end
|
|
396
378
|
|
|
397
379
|
def calc_time=(time)
|
|
398
|
-
|
|
380
|
+
redis.hset(experiment_config_key, :calc_time, time)
|
|
399
381
|
end
|
|
400
382
|
|
|
401
383
|
def calc_time
|
|
402
|
-
|
|
384
|
+
redis.hget(experiment_config_key, :calc_time).to_i
|
|
403
385
|
end
|
|
404
386
|
|
|
405
387
|
def jstring(goal = nil)
|
|
@@ -418,11 +400,11 @@ module Split
|
|
|
418
400
|
end
|
|
419
401
|
|
|
420
402
|
def load_metadata_from_configuration
|
|
421
|
-
|
|
403
|
+
Split.configuration.experiment_for(@name)[:metadata]
|
|
422
404
|
end
|
|
423
405
|
|
|
424
406
|
def load_metadata_from_redis
|
|
425
|
-
meta =
|
|
407
|
+
meta = redis.get(metadata_key)
|
|
426
408
|
JSON.parse(meta) unless meta.nil?
|
|
427
409
|
end
|
|
428
410
|
|
|
@@ -437,22 +419,49 @@ module Split
|
|
|
437
419
|
end
|
|
438
420
|
|
|
439
421
|
def load_alternatives_from_redis
|
|
440
|
-
case
|
|
422
|
+
case redis.type(@name)
|
|
441
423
|
when 'set' # convert legacy sets to lists
|
|
442
|
-
alts =
|
|
443
|
-
|
|
444
|
-
alts.reverse.each {|a|
|
|
445
|
-
|
|
424
|
+
alts = redis.smembers(@name)
|
|
425
|
+
redis.del(@name)
|
|
426
|
+
alts.reverse.each {|a| redis.lpush(@name, a) }
|
|
427
|
+
redis.lrange(@name, 0, -1)
|
|
446
428
|
else
|
|
447
|
-
|
|
429
|
+
redis.lrange(@name, 0, -1)
|
|
448
430
|
end
|
|
449
431
|
end
|
|
450
432
|
|
|
451
|
-
|
|
452
|
-
|
|
433
|
+
private
|
|
434
|
+
|
|
435
|
+
def redis
|
|
436
|
+
Split.redis
|
|
453
437
|
end
|
|
454
438
|
|
|
455
|
-
|
|
439
|
+
def redis_interface
|
|
440
|
+
RedisInterface.new
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
def persist_experiment_configuration
|
|
444
|
+
redis_interface.add_to_set(:experiments, name)
|
|
445
|
+
redis_interface.persist_list(name, @alternatives.map(&:name))
|
|
446
|
+
goals_collection.save
|
|
447
|
+
redis.set(metadata_key, @metadata.to_json) unless @metadata.nil?
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def remove_experiment_configuration
|
|
451
|
+
@alternatives.each(&:delete)
|
|
452
|
+
goals_collection.delete
|
|
453
|
+
delete_metadata
|
|
454
|
+
redis.del(@name)
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
def experiment_configuration_has_changed?
|
|
458
|
+
existing_alternatives = load_alternatives_from_redis
|
|
459
|
+
existing_goals = Split::GoalsCollection.new(@name).load_from_redis
|
|
460
|
+
existing_metadata = load_metadata_from_redis
|
|
461
|
+
existing_alternatives != @alternatives.map(&:name) ||
|
|
462
|
+
existing_goals != @goals ||
|
|
463
|
+
existing_metadata != @metadata
|
|
464
|
+
end
|
|
456
465
|
|
|
457
466
|
def goals_collection
|
|
458
467
|
Split::GoalsCollection.new(@name, @goals)
|