split 3.4.1 → 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 +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/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 -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 +34 -3
- data/split.gemspec +7 -7
- metadata +27 -35
- data/.travis.yml +0 -60
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,11 +66,15 @@ 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.
|
|
70
76
|
if defined?(::Rails)
|
|
71
|
-
class Railtie < Rails::Railtie
|
|
77
|
+
class Split::Railtie < Rails::Railtie
|
|
72
78
|
config.before_initialize { Split.configure {} }
|
|
73
79
|
end
|
|
74
80
|
else
|
data/spec/alternative_spec.rb
CHANGED
|
@@ -126,7 +126,7 @@ describe Split::Alternative do
|
|
|
126
126
|
|
|
127
127
|
it "should save to redis" do
|
|
128
128
|
alternative.save
|
|
129
|
-
expect(Split.redis.exists('basket_text:Basket')).to be true
|
|
129
|
+
expect(Split.redis.exists?('basket_text:Basket')).to be true
|
|
130
130
|
end
|
|
131
131
|
|
|
132
132
|
it "should increment participation count" do
|
data/spec/cache_spec.rb
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'spec_helper'
|
|
3
|
+
|
|
4
|
+
describe Split::Cache do
|
|
5
|
+
|
|
6
|
+
let(:namespace) { :test_namespace }
|
|
7
|
+
let(:key) { :test_key }
|
|
8
|
+
let(:now) { 1606189017 }
|
|
9
|
+
|
|
10
|
+
before { allow(Time).to receive(:now).and_return(now) }
|
|
11
|
+
|
|
12
|
+
describe 'clear' do
|
|
13
|
+
|
|
14
|
+
before { Split.configuration.cache = true }
|
|
15
|
+
|
|
16
|
+
it 'clears the cache' do
|
|
17
|
+
expect(Time).to receive(:now).and_return(now).exactly(2).times
|
|
18
|
+
Split::Cache.fetch(namespace, key) { Time.now }
|
|
19
|
+
Split::Cache.clear
|
|
20
|
+
Split::Cache.fetch(namespace, key) { Time.now }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe 'clear_key' do
|
|
25
|
+
before { Split.configuration.cache = true }
|
|
26
|
+
|
|
27
|
+
it 'clears the cache' do
|
|
28
|
+
expect(Time).to receive(:now).and_return(now).exactly(3).times
|
|
29
|
+
Split::Cache.fetch(namespace, :key1) { Time.now }
|
|
30
|
+
Split::Cache.fetch(namespace, :key2) { Time.now }
|
|
31
|
+
Split::Cache.clear_key(:key1)
|
|
32
|
+
|
|
33
|
+
Split::Cache.fetch(namespace, :key1) { Time.now }
|
|
34
|
+
Split::Cache.fetch(namespace, :key2) { Time.now }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
describe 'fetch' do
|
|
39
|
+
|
|
40
|
+
subject { Split::Cache.fetch(namespace, key) { Time.now } }
|
|
41
|
+
|
|
42
|
+
context 'when cache disabled' do
|
|
43
|
+
|
|
44
|
+
before { Split.configuration.cache = false }
|
|
45
|
+
|
|
46
|
+
it 'returns the yield' do
|
|
47
|
+
expect(subject).to eql(now)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it 'yields every time' do
|
|
51
|
+
expect(Time).to receive(:now).and_return(now).exactly(2).times
|
|
52
|
+
Split::Cache.fetch(namespace, key) { Time.now }
|
|
53
|
+
Split::Cache.fetch(namespace, key) { Time.now }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
context 'when cache enabled' do
|
|
58
|
+
|
|
59
|
+
before { Split.configuration.cache = true }
|
|
60
|
+
|
|
61
|
+
it 'returns the yield' do
|
|
62
|
+
expect(subject).to eql(now)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it 'yields once' do
|
|
66
|
+
expect(Time).to receive(:now).and_return(now).once
|
|
67
|
+
Split::Cache.fetch(namespace, key) { Time.now }
|
|
68
|
+
Split::Cache.fetch(namespace, key) { Time.now }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it 'honors namespace' do
|
|
72
|
+
expect(Split::Cache.fetch(:a, key) { :a }).to eql(:a)
|
|
73
|
+
expect(Split::Cache.fetch(:b, key) { :b }).to eql(:b)
|
|
74
|
+
|
|
75
|
+
expect(Split::Cache.fetch(:a, key) { :a }).to eql(:a)
|
|
76
|
+
expect(Split::Cache.fetch(:b, key) { :b }).to eql(:b)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it 'honors key' do
|
|
80
|
+
expect(Split::Cache.fetch(namespace, :a) { :a }).to eql(:a)
|
|
81
|
+
expect(Split::Cache.fetch(namespace, :b) { :b }).to eql(:b)
|
|
82
|
+
|
|
83
|
+
expect(Split::Cache.fetch(namespace, :a) { :a }).to eql(:a)
|
|
84
|
+
expect(Split::Cache.fetch(namespace, :b) { :b }).to eql(:b)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
data/spec/configuration_spec.rb
CHANGED
|
@@ -212,23 +212,12 @@ describe Split::Configuration do
|
|
|
212
212
|
expect(@config.normalized_experiments).to eq({:my_experiment=>{:alternatives=>[{"control_opt"=>0.67}, [{"second_opt"=>0.1}, {"third_opt"=>0.23}]]}})
|
|
213
213
|
end
|
|
214
214
|
|
|
215
|
-
context 'redis_url configuration [DEPRECATED]' do
|
|
216
|
-
it 'should warn on set and assign to #redis' do
|
|
217
|
-
expect(@config).to receive(:warn).with(/\[DEPRECATED\]/) { nil }
|
|
218
|
-
@config.redis_url = 'example_url'
|
|
219
|
-
expect(@config.redis).to eq('example_url')
|
|
220
|
-
end
|
|
221
|
-
|
|
222
|
-
it 'should warn on get and return #redis' do
|
|
223
|
-
expect(@config).to receive(:warn).with(/\[DEPRECATED\]/) { nil }
|
|
224
|
-
@config.redis = 'example_url'
|
|
225
|
-
expect(@config.redis_url).to eq('example_url')
|
|
226
|
-
end
|
|
227
|
-
end
|
|
228
|
-
|
|
229
215
|
context "redis configuration" do
|
|
230
216
|
it "should default to local redis server" do
|
|
231
|
-
|
|
217
|
+
old_redis_url = ENV['REDIS_URL']
|
|
218
|
+
ENV.delete('REDIS_URL')
|
|
219
|
+
expect(Split::Configuration.new.redis).to eq("redis://localhost:6379")
|
|
220
|
+
ENV['REDIS_URL'] = old_redis_url
|
|
232
221
|
end
|
|
233
222
|
|
|
234
223
|
it "should allow for redis url to be configured" do
|
|
@@ -238,8 +227,10 @@ describe Split::Configuration do
|
|
|
238
227
|
|
|
239
228
|
context "provided REDIS_URL environment variable" do
|
|
240
229
|
it "should use the ENV variable" do
|
|
230
|
+
old_redis_url = ENV['REDIS_URL']
|
|
241
231
|
ENV['REDIS_URL'] = "env_redis_url"
|
|
242
232
|
expect(Split::Configuration.new.redis).to eq("env_redis_url")
|
|
233
|
+
ENV['REDIS_URL'] = old_redis_url
|
|
243
234
|
end
|
|
244
235
|
end
|
|
245
236
|
end
|
|
@@ -255,4 +246,15 @@ describe Split::Configuration do
|
|
|
255
246
|
end
|
|
256
247
|
end
|
|
257
248
|
|
|
249
|
+
context "persistence cookie domain" do
|
|
250
|
+
it "should default to nil" do
|
|
251
|
+
expect(@config.persistence_cookie_domain).to eq(nil)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
it "should allow the persistence cookie domain to be configured" do
|
|
255
|
+
@config.persistence_cookie_domain = '.acme.com'
|
|
256
|
+
expect(@config.persistence_cookie_domain).to eq('.acme.com')
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
258
260
|
end
|
data/spec/dashboard_spec.rb
CHANGED
|
@@ -6,8 +6,16 @@ require 'split/dashboard'
|
|
|
6
6
|
describe Split::Dashboard do
|
|
7
7
|
include Rack::Test::Methods
|
|
8
8
|
|
|
9
|
+
class TestDashboard < Split::Dashboard
|
|
10
|
+
include Split::Helper
|
|
11
|
+
|
|
12
|
+
get '/my_experiment' do
|
|
13
|
+
ab_test(params[:experiment], 'blue', 'red')
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
9
17
|
def app
|
|
10
|
-
@app ||=
|
|
18
|
+
@app ||= TestDashboard
|
|
11
19
|
end
|
|
12
20
|
|
|
13
21
|
def link(color)
|
|
@@ -90,8 +98,17 @@ describe Split::Dashboard do
|
|
|
90
98
|
it "should set current user's alternative" do
|
|
91
99
|
blue_link.participant_count = 7
|
|
92
100
|
post "/force_alternative?experiment=#{experiment.name}", alternative: "blue"
|
|
93
|
-
|
|
94
|
-
|
|
101
|
+
|
|
102
|
+
get "/my_experiment?experiment=#{experiment.name}"
|
|
103
|
+
expect(last_response.body).to include("blue")
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
it "should not modify an existing user" do
|
|
107
|
+
blue_link.participant_count = 7
|
|
108
|
+
post "/force_alternative?experiment=#{experiment.name}", alternative: "blue"
|
|
109
|
+
|
|
110
|
+
expect(user[experiment.key]).to eq("red")
|
|
111
|
+
expect(blue_link.participant_count).to eq(7)
|
|
95
112
|
end
|
|
96
113
|
end
|
|
97
114
|
|
|
@@ -108,8 +125,9 @@ describe Split::Dashboard do
|
|
|
108
125
|
it "should set current user's alternative" do
|
|
109
126
|
blue_link.participant_count = 7
|
|
110
127
|
post "/force_alternative?experiment=#{experiment.name}", alternative: "blue"
|
|
111
|
-
|
|
112
|
-
|
|
128
|
+
|
|
129
|
+
get "/my_experiment?experiment=#{experiment.name}"
|
|
130
|
+
expect(last_response.body).to include("blue")
|
|
113
131
|
end
|
|
114
132
|
end
|
|
115
133
|
end
|
|
@@ -161,6 +179,28 @@ describe Split::Dashboard do
|
|
|
161
179
|
end
|
|
162
180
|
end
|
|
163
181
|
|
|
182
|
+
describe "update cohorting" do
|
|
183
|
+
it "calls enable of cohorting when action is enable" do
|
|
184
|
+
post "/update_cohorting?experiment=#{experiment.name}", { "cohorting_action": "enable" }
|
|
185
|
+
|
|
186
|
+
expect(experiment.cohorting_disabled?).to eq false
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
it "calls disable of cohorting when action is disable" do
|
|
190
|
+
post "/update_cohorting?experiment=#{experiment.name}", { "cohorting_action": "disable" }
|
|
191
|
+
|
|
192
|
+
expect(experiment.cohorting_disabled?).to eq true
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
it "calls neither enable or disable cohorting when passed invalid action" do
|
|
196
|
+
previous_value = experiment.cohorting_disabled?
|
|
197
|
+
|
|
198
|
+
post "/update_cohorting?experiment=#{experiment.name}", { "cohorting_action": "other" }
|
|
199
|
+
|
|
200
|
+
expect(experiment.cohorting_disabled?).to eq previous_value
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
164
204
|
it "should reset an experiment" do
|
|
165
205
|
red_link.participant_count = 5
|
|
166
206
|
blue_link.participant_count = 7
|
|
@@ -21,7 +21,7 @@ describe Split::EncapsulatedHelper do
|
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
it "calls the block with selected alternative" do
|
|
24
|
-
expect{|block| ab_test('link_color', 'red', 'red', &block) }.to yield_with_args('red',
|
|
24
|
+
expect{|block| ab_test('link_color', 'red', 'red', &block) }.to yield_with_args('red', {})
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
context "inside a view" do
|
data/spec/experiment_spec.rb
CHANGED
|
@@ -37,7 +37,7 @@ describe Split::Experiment do
|
|
|
37
37
|
|
|
38
38
|
it "should save to redis" do
|
|
39
39
|
experiment.save
|
|
40
|
-
expect(Split.redis.exists('basket_text')).to be true
|
|
40
|
+
expect(Split.redis.exists?('basket_text')).to be true
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
it "should save the start time to redis" do
|
|
@@ -85,7 +85,7 @@ describe Split::Experiment do
|
|
|
85
85
|
it "should not create duplicates when saving multiple times" do
|
|
86
86
|
experiment.save
|
|
87
87
|
experiment.save
|
|
88
|
-
expect(Split.redis.exists('basket_text')).to be true
|
|
88
|
+
expect(Split.redis.exists?('basket_text')).to be true
|
|
89
89
|
expect(Split.redis.lrange('basket_text', 0, -1)).to eq(['{"Basket":1}', '{"Cart":1}'])
|
|
90
90
|
end
|
|
91
91
|
|
|
@@ -118,6 +118,23 @@ describe Split::Experiment do
|
|
|
118
118
|
experiment = Split::Experiment.new('basket_text', :alternatives => ['Basket', "Cart"], :resettable => false)
|
|
119
119
|
expect(experiment.resettable).to be_falsey
|
|
120
120
|
end
|
|
121
|
+
|
|
122
|
+
context 'from configuration' do
|
|
123
|
+
let(:experiment_name) { :my_experiment }
|
|
124
|
+
let(:experiments) do
|
|
125
|
+
{
|
|
126
|
+
experiment_name => {
|
|
127
|
+
:alternatives => ['Control Opt', 'Alt one']
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
before { Split.configuration.experiments = experiments }
|
|
133
|
+
|
|
134
|
+
it 'assigns default values to the experiment' do
|
|
135
|
+
expect(Split::Experiment.new(experiment_name).resettable).to eq(true)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
121
138
|
end
|
|
122
139
|
|
|
123
140
|
describe 'persistent configuration' do
|
|
@@ -134,10 +151,23 @@ describe Split::Experiment do
|
|
|
134
151
|
|
|
135
152
|
describe '#metadata' do
|
|
136
153
|
let(:experiment) { Split::Experiment.new('basket_text', :alternatives => ['Basket', "Cart"], :algorithm => Split::Algorithms::Whiplash, :metadata => meta) }
|
|
154
|
+
let(:meta) { { a: 'b' }}
|
|
155
|
+
|
|
156
|
+
before do
|
|
157
|
+
experiment.save
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
it "should delete the key when metadata is removed" do
|
|
161
|
+
experiment.metadata = nil
|
|
162
|
+
experiment.save
|
|
163
|
+
|
|
164
|
+
expect(Split.redis.exists?(experiment.metadata_key)).to be_falsey
|
|
165
|
+
end
|
|
166
|
+
|
|
137
167
|
context 'simple hash' do
|
|
138
168
|
let(:meta) { { 'basket' => 'a', 'cart' => 'b' } }
|
|
169
|
+
|
|
139
170
|
it "should persist metadata in redis" do
|
|
140
|
-
experiment.save
|
|
141
171
|
e = Split::ExperimentCatalog.find('basket_text')
|
|
142
172
|
expect(e).to eq(experiment)
|
|
143
173
|
expect(e.metadata).to eq(meta)
|
|
@@ -147,7 +177,6 @@ describe Split::Experiment do
|
|
|
147
177
|
context 'nested hash' do
|
|
148
178
|
let(:meta) { { 'basket' => { 'one' => 'two' }, 'cart' => 'b' } }
|
|
149
179
|
it "should persist metadata in redis" do
|
|
150
|
-
experiment.save
|
|
151
180
|
e = Split::ExperimentCatalog.find('basket_text')
|
|
152
181
|
expect(e).to eq(experiment)
|
|
153
182
|
expect(e.metadata).to eq(meta)
|
|
@@ -180,7 +209,7 @@ describe Split::Experiment do
|
|
|
180
209
|
experiment.save
|
|
181
210
|
|
|
182
211
|
experiment.delete
|
|
183
|
-
expect(Split.redis.exists('link_color')).to be false
|
|
212
|
+
expect(Split.redis.exists?('link_color')).to be false
|
|
184
213
|
expect(Split::ExperimentCatalog.find('link_color')).to be_nil
|
|
185
214
|
end
|
|
186
215
|
|
|
@@ -206,8 +235,14 @@ describe Split::Experiment do
|
|
|
206
235
|
experiment.delete
|
|
207
236
|
expect(experiment.start_time).to be_nil
|
|
208
237
|
end
|
|
209
|
-
end
|
|
210
238
|
|
|
239
|
+
it "should default cohorting back to false" do
|
|
240
|
+
experiment.disable_cohorting
|
|
241
|
+
expect(experiment.cohorting_disabled?).to eq(true)
|
|
242
|
+
experiment.delete
|
|
243
|
+
expect(experiment.cohorting_disabled?).to eq(false)
|
|
244
|
+
end
|
|
245
|
+
end
|
|
211
246
|
|
|
212
247
|
describe 'winner' do
|
|
213
248
|
it "should have no winner initially" do
|
|
@@ -216,12 +251,17 @@ describe Split::Experiment do
|
|
|
216
251
|
end
|
|
217
252
|
|
|
218
253
|
describe 'winner=' do
|
|
219
|
-
it
|
|
254
|
+
it 'should allow you to specify a winner' do
|
|
220
255
|
experiment.save
|
|
221
256
|
experiment.winner = 'red'
|
|
222
257
|
expect(experiment.winner.name).to eq('red')
|
|
223
258
|
end
|
|
224
259
|
|
|
260
|
+
it 'should call the on_experiment_winner_choose hook' do
|
|
261
|
+
expect(Split.configuration.on_experiment_winner_choose).to receive(:call)
|
|
262
|
+
experiment.winner = 'green'
|
|
263
|
+
end
|
|
264
|
+
|
|
225
265
|
context 'when has_winner state is memoized' do
|
|
226
266
|
before { expect(experiment).to_not have_winner }
|
|
227
267
|
|
|
@@ -370,6 +410,22 @@ describe Split::Experiment do
|
|
|
370
410
|
end
|
|
371
411
|
end
|
|
372
412
|
|
|
413
|
+
describe "#cohorting_disabled?" do
|
|
414
|
+
it "returns false when nothing has been configured" do
|
|
415
|
+
expect(experiment.cohorting_disabled?).to eq false
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
it "returns true when enable_cohorting is performed" do
|
|
419
|
+
experiment.enable_cohorting
|
|
420
|
+
expect(experiment.cohorting_disabled?).to eq false
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
it "returns false when nothing has been configured" do
|
|
424
|
+
experiment.disable_cohorting
|
|
425
|
+
expect(experiment.cohorting_disabled?).to eq true
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
|
|
373
429
|
describe 'changing an existing experiment' do
|
|
374
430
|
def same_but_different_alternative
|
|
375
431
|
Split::ExperimentCatalog.find_or_create('link_color', 'blue', 'yellow', 'orange')
|
|
@@ -392,6 +448,21 @@ describe Split::Experiment do
|
|
|
392
448
|
expect(same_experiment_again.version).to eq(1)
|
|
393
449
|
end
|
|
394
450
|
|
|
451
|
+
context "when metadata is changed" do
|
|
452
|
+
it "should increase version" do
|
|
453
|
+
experiment.save
|
|
454
|
+
experiment.metadata = { 'foo' => 'bar' }
|
|
455
|
+
|
|
456
|
+
expect { experiment.save }.to change { experiment.version }.by(1)
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
it "does not increase version" do
|
|
460
|
+
experiment.metadata = nil
|
|
461
|
+
experiment.save
|
|
462
|
+
expect { experiment.save }.to change { experiment.version }.by(0)
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
|
|
395
466
|
context 'when experiment configuration is changed' do
|
|
396
467
|
let(:reset_manually) { false }
|
|
397
468
|
|
data/spec/helper_spec.rb
CHANGED
|
@@ -229,13 +229,15 @@ describe Split::Helper do
|
|
|
229
229
|
|
|
230
230
|
context "when user already has experiment" do
|
|
231
231
|
let(:mock_user){ Split::User.new(self, {'test_0' => 'test-alt'}) }
|
|
232
|
-
|
|
232
|
+
|
|
233
|
+
before do
|
|
233
234
|
Split.configure do |config|
|
|
234
235
|
config.allow_multiple_experiments = 'control'
|
|
235
236
|
end
|
|
237
|
+
|
|
236
238
|
Split::ExperimentCatalog.find_or_initialize('test_0', 'control', 'test-alt').save
|
|
237
239
|
Split::ExperimentCatalog.find_or_initialize('test_1', 'control', 'test-alt').save
|
|
238
|
-
|
|
240
|
+
end
|
|
239
241
|
|
|
240
242
|
it "should restore previously selected alternative" do
|
|
241
243
|
expect(ab_user.active_experiments.size).to eq 1
|
|
@@ -243,6 +245,16 @@ describe Split::Helper do
|
|
|
243
245
|
expect(ab_test(:test_0, {'control' => 1}, {"test-alt" => 100})).to eq 'test-alt'
|
|
244
246
|
end
|
|
245
247
|
|
|
248
|
+
it "should select the correct alternatives after experiment resets" do
|
|
249
|
+
experiment = Split::ExperimentCatalog.find(:test_0)
|
|
250
|
+
experiment.reset
|
|
251
|
+
mock_user[experiment.key] = 'test-alt'
|
|
252
|
+
|
|
253
|
+
expect(ab_user.active_experiments.size).to eq 1
|
|
254
|
+
expect(ab_test(:test_0, {'control' => 100}, {"test-alt" => 1})).to eq 'test-alt'
|
|
255
|
+
expect(ab_test(:test_0, {'control' => 0}, {"test-alt" => 100})).to eq 'test-alt'
|
|
256
|
+
end
|
|
257
|
+
|
|
246
258
|
it "lets override existing choice" do
|
|
247
259
|
pending "this requires user store reset on first call not depending on whelther it is current trial"
|
|
248
260
|
@params = { 'ab_test' => { 'test_1' => 'test-alt' } }
|
|
@@ -265,33 +277,63 @@ describe Split::Helper do
|
|
|
265
277
|
end
|
|
266
278
|
|
|
267
279
|
describe 'metadata' do
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
:
|
|
272
|
-
|
|
273
|
-
|
|
280
|
+
context 'is defined' do
|
|
281
|
+
before do
|
|
282
|
+
Split.configuration.experiments = {
|
|
283
|
+
:my_experiment => {
|
|
284
|
+
:alternatives => ["one", "two"],
|
|
285
|
+
:resettable => false,
|
|
286
|
+
:metadata => { 'one' => 'Meta1', 'two' => 'Meta2' }
|
|
287
|
+
}
|
|
274
288
|
}
|
|
275
|
-
|
|
276
|
-
end
|
|
289
|
+
end
|
|
277
290
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
291
|
+
it 'should be passed to helper block' do
|
|
292
|
+
@params = { 'ab_test' => { 'my_experiment' => 'two' } }
|
|
293
|
+
expect(ab_test('my_experiment')).to eq 'two'
|
|
294
|
+
expect(ab_test('my_experiment') do |alternative, meta|
|
|
295
|
+
meta
|
|
296
|
+
end).to eq('Meta2')
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
it 'should pass control metadata helper block if library disabled' do
|
|
300
|
+
Split.configure do |config|
|
|
301
|
+
config.enabled = false
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
expect(ab_test('my_experiment')).to eq 'one'
|
|
305
|
+
expect(ab_test('my_experiment') do |_, meta|
|
|
306
|
+
meta
|
|
307
|
+
end).to eq('Meta1')
|
|
308
|
+
end
|
|
284
309
|
end
|
|
285
310
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
311
|
+
context 'is not defined' do
|
|
312
|
+
before do
|
|
313
|
+
Split.configuration.experiments = {
|
|
314
|
+
:my_experiment => {
|
|
315
|
+
:alternatives => ["one", "two"],
|
|
316
|
+
:resettable => false,
|
|
317
|
+
:metadata => nil
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
it 'should be passed to helper block' do
|
|
323
|
+
expect(ab_test('my_experiment') do |alternative, meta|
|
|
324
|
+
meta
|
|
325
|
+
end).to eq({})
|
|
289
326
|
end
|
|
290
327
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
328
|
+
it 'should pass control metadata helper block if library disabled' do
|
|
329
|
+
Split.configure do |config|
|
|
330
|
+
config.enabled = false
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
expect(ab_test('my_experiment') do |_, meta|
|
|
334
|
+
meta
|
|
335
|
+
end).to eq({})
|
|
336
|
+
end
|
|
295
337
|
end
|
|
296
338
|
end
|
|
297
339
|
|
|
@@ -1096,15 +1138,9 @@ describe Split::Helper do
|
|
|
1096
1138
|
end
|
|
1097
1139
|
|
|
1098
1140
|
it "should increment the counter for the specified-goal completed alternative" do
|
|
1099
|
-
expect(
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
}).not_to change {
|
|
1103
|
-
Split::Alternative.new(@alternative_name, @experiment_name).completed_count(@goal2)
|
|
1104
|
-
}
|
|
1105
|
-
}).to change {
|
|
1106
|
-
Split::Alternative.new(@alternative_name, @experiment_name).completed_count(@goal1)
|
|
1107
|
-
}.by(1)
|
|
1141
|
+
expect{ ab_finished({"link_color" => ["purchase"]}) }
|
|
1142
|
+
.to change{ Split::Alternative.new(@alternative_name, @experiment_name).completed_count(@goal2) }.by(0)
|
|
1143
|
+
.and change{ Split::Alternative.new(@alternative_name, @experiment_name).completed_count(@goal1) }.by(1)
|
|
1108
1144
|
end
|
|
1109
1145
|
end
|
|
1110
1146
|
end
|
|
@@ -52,7 +52,7 @@ describe Split::Persistence::CookieAdapter do
|
|
|
52
52
|
it "puts multiple experiments in a single cookie" do
|
|
53
53
|
subject["foo"] = "FOO"
|
|
54
54
|
subject["bar"] = "BAR"
|
|
55
|
-
expect(context.response.headers["Set-Cookie"]).to match(/\Asplit=%7B%22foo%22%3A%22FOO%22%2C%22bar%22%3A%22BAR%22%7D; path=\/; expires=[a-zA-Z]{3}, \d{2} [a-zA-Z]{3} \d{4} \d{2}:\d{2}:\d{2} -
|
|
55
|
+
expect(context.response.headers["Set-Cookie"]).to match(/\Asplit=%7B%22foo%22%3A%22FOO%22%2C%22bar%22%3A%22BAR%22%7D; path=\/; expires=[a-zA-Z]{3}, \d{2} [a-zA-Z]{3} \d{4} \d{2}:\d{2}:\d{2} [A-Z]{3}\Z/)
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
it "ensure other added cookies are not overriden" do
|
|
@@ -60,6 +60,15 @@ describe Split::Persistence::RedisAdapter do
|
|
|
60
60
|
end
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
+
describe '#find' do
|
|
64
|
+
before { Split::Persistence::RedisAdapter.with_config(:lookup_by => proc{'frag'}, :namespace => 'a_namespace') }
|
|
65
|
+
|
|
66
|
+
it "should create and user from a given key" do
|
|
67
|
+
adapter = Split::Persistence::RedisAdapter.find(2)
|
|
68
|
+
expect(adapter.redis_key).to eq("a_namespace:2")
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
63
72
|
context 'functional tests' do
|
|
64
73
|
before { Split::Persistence::RedisAdapter.with_config(:lookup_by => 'lookup') }
|
|
65
74
|
|