split 2.2.0 → 3.1.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/.travis.yml +27 -3
- data/Appraisals +6 -5
- data/CHANGELOG.md +48 -0
- data/CONTRIBUTING.md +54 -5
- data/LICENSE +1 -1
- data/README.md +183 -112
- data/gemfiles/4.2.gemfile +1 -1
- data/gemfiles/5.0.gemfile +2 -2
- data/gemfiles/{4.1.gemfile → 5.1.gemfile} +3 -2
- data/lib/split/algorithms/block_randomization.rb +22 -0
- data/lib/split/alternative.rb +29 -7
- data/lib/split/combined_experiments_helper.rb +30 -0
- data/lib/split/configuration.rb +5 -0
- data/lib/split/dashboard/helpers.rb +5 -1
- data/lib/split/dashboard/views/_experiment.erb +31 -1
- data/lib/split/encapsulated_helper.rb +2 -0
- data/lib/split/engine.rb +2 -0
- data/lib/split/helper.rb +29 -0
- data/lib/split/persistence/cookie_adapter.rb +19 -15
- data/lib/split/persistence/dual_adapter.rb +3 -0
- data/lib/split/persistence.rb +5 -3
- data/lib/split/user.rb +2 -0
- data/lib/split/version.rb +3 -3
- data/lib/split/zscore.rb +1 -1
- data/lib/split.rb +21 -19
- data/spec/algorithms/block_randomization_spec.rb +32 -0
- data/spec/alternative_spec.rb +31 -0
- data/spec/combined_experiments_helper_spec.rb +57 -0
- data/spec/dashboard_helpers_spec.rb +14 -0
- data/spec/experiment_spec.rb +1 -3
- data/spec/helper_spec.rb +12 -0
- data/spec/persistence/cookie_adapter_spec.rb +6 -2
- data/spec/persistence/dual_adapter_spec.rb +2 -2
- data/split.gemspec +13 -3
- metadata +23 -13
- data/lib/split/algorithms.rb +0 -4
- data/lib/split/extensions.rb +0 -4
|
@@ -5,6 +5,25 @@
|
|
|
5
5
|
<% end %>
|
|
6
6
|
|
|
7
7
|
<% experiment.calc_winning_alternatives %>
|
|
8
|
+
<%
|
|
9
|
+
extra_columns = []
|
|
10
|
+
experiment.alternatives.each do |alternative|
|
|
11
|
+
extra_info = alternative.extra_info || {}
|
|
12
|
+
extra_columns += extra_info.keys
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
extra_columns.uniq!
|
|
16
|
+
summary_texts = {}
|
|
17
|
+
extra_columns.each do |column|
|
|
18
|
+
extra_infos = experiment.alternatives.map(&:extra_info).select{|extra_info| extra_info && extra_info[column] }
|
|
19
|
+
if extra_infos[0][column].kind_of?(Numeric)
|
|
20
|
+
summary_texts[column] = extra_infos.inject(0){|sum, extra_info| sum += extra_info[column]}
|
|
21
|
+
else
|
|
22
|
+
summary_texts[column] = "N/A"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
%>
|
|
26
|
+
|
|
8
27
|
|
|
9
28
|
<div class="<%= experiment_class %>" data-name="<%= experiment.name %>" data-complete="<%= experiment.has_winner? %>">
|
|
10
29
|
<div class="experiment-header">
|
|
@@ -32,6 +51,9 @@
|
|
|
32
51
|
<th>Non-finished</th>
|
|
33
52
|
<th>Completed</th>
|
|
34
53
|
<th>Conversion Rate</th>
|
|
54
|
+
<% extra_columns.each do |column| %>
|
|
55
|
+
<th><%= column %></th>
|
|
56
|
+
<% end %>
|
|
35
57
|
<th>
|
|
36
58
|
<form>
|
|
37
59
|
<select id="dropdown-<%=experiment.jstring(goal)%>" name="dropdown-<%=experiment.jstring(goal)%>">
|
|
@@ -82,6 +104,9 @@
|
|
|
82
104
|
});
|
|
83
105
|
});
|
|
84
106
|
</script>
|
|
107
|
+
<% extra_columns.each do |column| %>
|
|
108
|
+
<td><%= alternative.extra_info && alternative.extra_info[column] %></td>
|
|
109
|
+
<% end %>
|
|
85
110
|
<td>
|
|
86
111
|
<div class="box-<%=experiment.jstring(goal)%> confidence-<%=experiment.jstring(goal)%>">
|
|
87
112
|
<span title='z-score: <%= round(alternative.z_score(goal), 3) %>'><%= confidence_level(alternative.z_score(goal)) %></span>
|
|
@@ -90,7 +115,7 @@
|
|
|
90
115
|
<div class="box-<%=experiment.jstring(goal)%> probability-<%=experiment.jstring(goal)%>">
|
|
91
116
|
<span title="p_winner: <%= round(alternative.p_winner(goal), 3) %>"><%= number_to_percentage(round(alternative.p_winner(goal), 3)) %>%</span>
|
|
92
117
|
</div>
|
|
93
|
-
</td>
|
|
118
|
+
</td>
|
|
94
119
|
<td>
|
|
95
120
|
<% if experiment.has_winner? %>
|
|
96
121
|
<% if experiment.winner.name == alternative.name %>
|
|
@@ -118,6 +143,11 @@
|
|
|
118
143
|
<td><%= total_unfinished %></td>
|
|
119
144
|
<td><%= total_completed %></td>
|
|
120
145
|
<td>N/A</td>
|
|
146
|
+
<% extra_columns.each do |column| %>
|
|
147
|
+
<td>
|
|
148
|
+
<%= summary_texts[column] %>
|
|
149
|
+
</td>
|
|
150
|
+
<% end %>
|
|
121
151
|
<td>N/A</td>
|
|
122
152
|
<td>N/A</td>
|
|
123
153
|
</tr>
|
data/lib/split/engine.rb
CHANGED
|
@@ -5,6 +5,8 @@ module Split
|
|
|
5
5
|
if Split.configuration.include_rails_helper
|
|
6
6
|
ActionController::Base.send :include, Split::Helper
|
|
7
7
|
ActionController::Base.helper Split::Helper
|
|
8
|
+
ActionController::Base.send :include, Split::CombinedExperimentsHelper
|
|
9
|
+
ActionController::Base.helper Split::CombinedExperimentsHelper
|
|
8
10
|
end
|
|
9
11
|
end
|
|
10
12
|
end
|
data/lib/split/helper.rb
CHANGED
|
@@ -10,6 +10,7 @@ module Split
|
|
|
10
10
|
experiment = ExperimentCatalog.find_or_initialize(metric_descriptor, control, *alternatives)
|
|
11
11
|
alternative = if Split.configuration.enabled
|
|
12
12
|
experiment.save
|
|
13
|
+
raise(Split::InvalidExperimentsFormatError) unless (Split.configuration.experiments || {}).fetch(experiment.name.to_sym, {})[:combined_experiments].nil?
|
|
13
14
|
trial = Trial.new(:user => ab_user, :experiment => experiment,
|
|
14
15
|
:override => override_alternative(experiment.name), :exclude => exclude_visitor?,
|
|
15
16
|
:disabled => split_generically_disabled?)
|
|
@@ -76,6 +77,34 @@ module Split
|
|
|
76
77
|
Split.configuration.db_failover_on_db_error.call(e)
|
|
77
78
|
end
|
|
78
79
|
|
|
80
|
+
def ab_record_extra_info(metric_descriptor, key, value = 1)
|
|
81
|
+
return if exclude_visitor? || Split.configuration.disabled?
|
|
82
|
+
metric_descriptor, goals = normalize_metric(metric_descriptor)
|
|
83
|
+
experiments = Metric.possible_experiments(metric_descriptor)
|
|
84
|
+
|
|
85
|
+
if experiments.any?
|
|
86
|
+
experiments.each do |experiment|
|
|
87
|
+
alternative_name = ab_user[experiment.key]
|
|
88
|
+
|
|
89
|
+
if alternative_name
|
|
90
|
+
alternative = experiment.alternatives.find{|alt| alt.name == alternative_name}
|
|
91
|
+
alternative.record_extra_info(key, value) if alternative
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
rescue => e
|
|
96
|
+
raise unless Split.configuration.db_failover
|
|
97
|
+
Split.configuration.db_failover_on_db_error.call(e)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def ab_active_experiments()
|
|
101
|
+
ab_user.active_experiments
|
|
102
|
+
rescue => e
|
|
103
|
+
raise unless Split.configuration.db_failover
|
|
104
|
+
Split.configuration.db_failover_on_db_error.call(e)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
|
|
79
108
|
def override_present?(experiment_name)
|
|
80
109
|
override_alternative(experiment_name)
|
|
81
110
|
end
|
|
@@ -6,20 +6,21 @@ module Split
|
|
|
6
6
|
class CookieAdapter
|
|
7
7
|
|
|
8
8
|
def initialize(context)
|
|
9
|
-
@
|
|
9
|
+
@request, @response = context.request, context.response
|
|
10
|
+
@cookies = @request.cookies
|
|
10
11
|
@expires = Time.now + cookie_length_config
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
def [](key)
|
|
14
|
-
hash[key]
|
|
15
|
+
hash[key.to_s]
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
def []=(key, value)
|
|
18
|
-
set_cookie(hash.merge(key => value))
|
|
19
|
+
set_cookie(hash.merge!(key.to_s => value))
|
|
19
20
|
end
|
|
20
21
|
|
|
21
22
|
def delete(key)
|
|
22
|
-
set_cookie(hash.tap { |h| h.delete(key) })
|
|
23
|
+
set_cookie(hash.tap { |h| h.delete(key.to_s) })
|
|
23
24
|
end
|
|
24
25
|
|
|
25
26
|
def keys
|
|
@@ -28,22 +29,25 @@ module Split
|
|
|
28
29
|
|
|
29
30
|
private
|
|
30
31
|
|
|
31
|
-
def set_cookie(value)
|
|
32
|
-
@
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
def set_cookie(value = {})
|
|
33
|
+
@response.set_cookie :split.to_s, default_options.merge(value: JSON.generate(value))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def default_options
|
|
37
|
+
{ expires: @expires, path: '/' }
|
|
36
38
|
end
|
|
37
39
|
|
|
38
40
|
def hash
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
@hash ||= begin
|
|
42
|
+
if cookies = @cookies[:split.to_s]
|
|
43
|
+
begin
|
|
44
|
+
JSON.parse(cookies)
|
|
45
|
+
rescue JSON::ParserError
|
|
46
|
+
{}
|
|
47
|
+
end
|
|
48
|
+
else
|
|
43
49
|
{}
|
|
44
50
|
end
|
|
45
|
-
else
|
|
46
|
-
{}
|
|
47
51
|
end
|
|
48
52
|
end
|
|
49
53
|
|
data/lib/split/persistence.rb
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
-
%w[session_adapter cookie_adapter redis_adapter dual_adapter].each do |f|
|
|
3
|
-
require "split/persistence/#{f}"
|
|
4
|
-
end
|
|
5
2
|
|
|
6
3
|
module Split
|
|
7
4
|
module Persistence
|
|
5
|
+
require 'split/persistence/cookie_adapter'
|
|
6
|
+
require 'split/persistence/dual_adapter'
|
|
7
|
+
require 'split/persistence/redis_adapter'
|
|
8
|
+
require 'split/persistence/session_adapter'
|
|
9
|
+
|
|
8
10
|
ADAPTERS = {
|
|
9
11
|
:cookie => Split::Persistence::CookieAdapter,
|
|
10
12
|
:session => Split::Persistence::SessionAdapter
|
data/lib/split/user.rb
CHANGED
data/lib/split/version.rb
CHANGED
data/lib/split/zscore.rb
CHANGED
data/lib/split.rb
CHANGED
|
@@ -1,24 +1,26 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
alternative
|
|
4
|
-
configuration
|
|
5
|
-
exceptions
|
|
6
|
-
experiment
|
|
7
|
-
experiment_catalog
|
|
8
|
-
extensions
|
|
9
|
-
goals_collection
|
|
10
|
-
helper
|
|
11
|
-
metric
|
|
12
|
-
persistence
|
|
13
|
-
encapsulated_helper
|
|
14
|
-
redis_interface
|
|
15
|
-
trial
|
|
16
|
-
user
|
|
17
|
-
version
|
|
18
|
-
zscore].each do |f|
|
|
19
|
-
require "split/#{f}"
|
|
20
|
-
end
|
|
2
|
+
require 'redis'
|
|
21
3
|
|
|
4
|
+
require 'split/algorithms/block_randomization'
|
|
5
|
+
require 'split/algorithms/weighted_sample'
|
|
6
|
+
require 'split/algorithms/whiplash'
|
|
7
|
+
require 'split/alternative'
|
|
8
|
+
require 'split/configuration'
|
|
9
|
+
require 'split/encapsulated_helper'
|
|
10
|
+
require 'split/exceptions'
|
|
11
|
+
require 'split/experiment'
|
|
12
|
+
require 'split/experiment_catalog'
|
|
13
|
+
require 'split/extensions/string'
|
|
14
|
+
require 'split/goals_collection'
|
|
15
|
+
require 'split/helper'
|
|
16
|
+
require 'split/combined_experiments_helper'
|
|
17
|
+
require 'split/metric'
|
|
18
|
+
require 'split/persistence'
|
|
19
|
+
require 'split/redis_interface'
|
|
20
|
+
require 'split/trial'
|
|
21
|
+
require 'split/user'
|
|
22
|
+
require 'split/version'
|
|
23
|
+
require 'split/zscore'
|
|
22
24
|
require 'split/engine' if defined?(Rails)
|
|
23
25
|
|
|
24
26
|
module Split
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
|
|
3
|
+
describe Split::Algorithms::BlockRandomization do
|
|
4
|
+
|
|
5
|
+
let(:experiment) { Split::Experiment.new 'experiment' }
|
|
6
|
+
let(:alternative_A) { Split::Alternative.new 'A', 'experiment' }
|
|
7
|
+
let(:alternative_B) { Split::Alternative.new 'B', 'experiment' }
|
|
8
|
+
let(:alternative_C) { Split::Alternative.new 'C', 'experiment' }
|
|
9
|
+
|
|
10
|
+
before :each do
|
|
11
|
+
allow(experiment).to receive(:alternatives) { [alternative_A, alternative_B, alternative_C] }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it "should return an alternative" do
|
|
15
|
+
expect(Split::Algorithms::BlockRandomization.choose_alternative(experiment).class).to eq(Split::Alternative)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it "should always return the minimum participation option" do
|
|
19
|
+
allow(alternative_A).to receive(:participant_count) { 1 }
|
|
20
|
+
allow(alternative_B).to receive(:participant_count) { 1 }
|
|
21
|
+
allow(alternative_C).to receive(:participant_count) { 0 }
|
|
22
|
+
expect(Split::Algorithms::BlockRandomization.choose_alternative(experiment)).to eq(alternative_C)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "should return one of the minimum participation options when multiple" do
|
|
26
|
+
allow(alternative_A).to receive(:participant_count) { 0 }
|
|
27
|
+
allow(alternative_B).to receive(:participant_count) { 0 }
|
|
28
|
+
allow(alternative_C).to receive(:participant_count) { 0 }
|
|
29
|
+
alternative = Split::Algorithms::BlockRandomization.choose_alternative(experiment)
|
|
30
|
+
expect([alternative_A, alternative_B, alternative_C].include?(alternative)).to be(true)
|
|
31
|
+
end
|
|
32
|
+
end
|
data/spec/alternative_spec.rb
CHANGED
|
@@ -274,4 +274,35 @@ describe Split::Alternative do
|
|
|
274
274
|
expect(control.z_score(goal2)).to eq('N/A')
|
|
275
275
|
end
|
|
276
276
|
end
|
|
277
|
+
|
|
278
|
+
describe "extra_info" do
|
|
279
|
+
it "reads saved value of recorded_info in redis" do
|
|
280
|
+
saved_recorded_info = {"key_1" => 1, "key_2" => "2"}
|
|
281
|
+
Split.redis.hset "#{alternative.experiment_name}:#{alternative.name}", 'recorded_info', saved_recorded_info.to_json
|
|
282
|
+
extra_info = alternative.extra_info
|
|
283
|
+
|
|
284
|
+
expect(extra_info).to eql(saved_recorded_info)
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
describe "record_extra_info" do
|
|
289
|
+
it "saves key" do
|
|
290
|
+
alternative.record_extra_info("signup", 1)
|
|
291
|
+
expect(alternative.extra_info["signup"]).to eql(1)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
it "adds value to saved key's value second argument is number" do
|
|
295
|
+
alternative.record_extra_info("signup", 1)
|
|
296
|
+
alternative.record_extra_info("signup", 2)
|
|
297
|
+
expect(alternative.extra_info["signup"]).to eql(3)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
it "sets saved's key value to the second argument if it's a string" do
|
|
301
|
+
alternative.record_extra_info("signup", "Value 1")
|
|
302
|
+
expect(alternative.extra_info["signup"]).to eql("Value 1")
|
|
303
|
+
|
|
304
|
+
alternative.record_extra_info("signup", "Value 2")
|
|
305
|
+
expect(alternative.extra_info["signup"]).to eql("Value 2")
|
|
306
|
+
end
|
|
307
|
+
end
|
|
277
308
|
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'spec_helper'
|
|
3
|
+
require 'split/combined_experiments_helper'
|
|
4
|
+
|
|
5
|
+
describe Split::CombinedExperimentsHelper do
|
|
6
|
+
include Split::CombinedExperimentsHelper
|
|
7
|
+
|
|
8
|
+
describe 'ab_combined_test' do
|
|
9
|
+
let!(:config_enabled) { true }
|
|
10
|
+
let!(:combined_experiments) { [:exp_1_click, :exp_1_scroll ]}
|
|
11
|
+
let!(:allow_multiple_experiments) { true }
|
|
12
|
+
|
|
13
|
+
before do
|
|
14
|
+
Split.configuration.experiments = {
|
|
15
|
+
:combined_exp_1 => {
|
|
16
|
+
:alternatives => [ {"control"=> 0.5}, {"test-alt"=> 0.5} ],
|
|
17
|
+
:metric => :my_metric,
|
|
18
|
+
:combined_experiments => combined_experiments
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
Split.configuration.enabled = config_enabled
|
|
22
|
+
Split.configuration.allow_multiple_experiments = allow_multiple_experiments
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
context 'without config enabled' do
|
|
26
|
+
let!(:config_enabled) { false }
|
|
27
|
+
|
|
28
|
+
it "raises an error" do
|
|
29
|
+
expect(lambda { ab_combined_test :combined_exp_1 }).to raise_error(Split::InvalidExperimentsFormatError )
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
context 'multiple experiments disabled' do
|
|
34
|
+
let!(:allow_multiple_experiments) { false }
|
|
35
|
+
|
|
36
|
+
it "raises an error if multiple experiments is disabled" do
|
|
37
|
+
expect(lambda { ab_combined_test :combined_exp_1 }).to raise_error(Split::InvalidExperimentsFormatError)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
context 'without combined experiments' do
|
|
42
|
+
let!(:combined_experiments) { nil }
|
|
43
|
+
|
|
44
|
+
it "raises an error" do
|
|
45
|
+
expect(lambda { ab_combined_test :combined_exp_1 }).to raise_error(Split::InvalidExperimentsFormatError )
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it "uses same alternatives for all sub experiments " do
|
|
50
|
+
allow(self).to receive(:get_alternative) { "test-alt" }
|
|
51
|
+
expect(self).to receive(:ab_test).with(:exp_1_click, {"control"=>0.5}, {"test-alt"=>0.5}) { "test-alt" }
|
|
52
|
+
expect(self).to receive(:ab_test).with(:exp_1_scroll, [{"test-alt" => 1}] )
|
|
53
|
+
|
|
54
|
+
ab_combined_test('combined_exp_1')
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -24,5 +24,19 @@ describe Split::DashboardHelpers do
|
|
|
24
24
|
expect(confidence_level(2.58)).to eq('99% confidence')
|
|
25
25
|
expect(confidence_level(3.00)).to eq('99% confidence')
|
|
26
26
|
end
|
|
27
|
+
|
|
28
|
+
describe '#round' do
|
|
29
|
+
it 'can round number strings' do
|
|
30
|
+
expect(round('3.1415')).to eq BigDecimal.new('3.14')
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'can round number strings for precsion' do
|
|
34
|
+
expect(round('3.1415', 1)).to eq BigDecimal.new('3.1')
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'can handle invalid number strings' do
|
|
38
|
+
expect(round('N/A')).to be_zero
|
|
39
|
+
end
|
|
40
|
+
end
|
|
27
41
|
end
|
|
28
42
|
end
|
data/spec/experiment_spec.rb
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
require 'spec_helper'
|
|
3
|
-
require 'split/experiment'
|
|
4
|
-
require 'split/algorithms'
|
|
5
3
|
require 'time'
|
|
6
4
|
|
|
7
5
|
describe Split::Experiment do
|
|
@@ -460,7 +458,7 @@ describe Split::Experiment do
|
|
|
460
458
|
expect(experiment.alternatives[0].p_winner).to be_within(0.04).of(0.50)
|
|
461
459
|
end
|
|
462
460
|
|
|
463
|
-
it "should calculate the probability of being the winning alternative separately for each goal" do
|
|
461
|
+
it "should calculate the probability of being the winning alternative separately for each goal", :skip => true do
|
|
464
462
|
experiment = Split::ExperimentCatalog.find_or_create({'link_color3' => ["purchase", "refund"]}, 'blue', 'red', 'green')
|
|
465
463
|
goal1 = experiment.goals[0]
|
|
466
464
|
goal2 = experiment.goals[1]
|
data/spec/helper_spec.rb
CHANGED
|
@@ -35,6 +35,18 @@ describe Split::Helper do
|
|
|
35
35
|
expect(lambda { ab_test({'link_color' => "purchase"}, 'blue', 'red') }).not_to raise_error
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
+
it "raises an appropriate error when processing combined expirements" do
|
|
39
|
+
Split.configuration.experiments = {
|
|
40
|
+
:combined_exp_1 => {
|
|
41
|
+
:alternatives => [ { name: "control", percent: 50 }, { name: "test-alt", percent: 50 } ],
|
|
42
|
+
:metric => :my_metric,
|
|
43
|
+
:combined_experiments => [:combined_exp_1_sub_1]
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
Split::ExperimentCatalog.find_or_create('combined_exp_1')
|
|
47
|
+
expect(lambda { ab_test('combined_exp_1')}).to raise_error(Split::InvalidExperimentsFormatError )
|
|
48
|
+
end
|
|
49
|
+
|
|
38
50
|
it "should assign a random alternative to a new user when there are an equal number of alternatives assigned" do
|
|
39
51
|
ab_test('link_color', 'blue', 'red')
|
|
40
52
|
expect(['red', 'blue']).to include(ab_user['link_color'])
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
require "spec_helper"
|
|
3
|
+
require 'rack/test'
|
|
3
4
|
|
|
4
5
|
describe Split::Persistence::CookieAdapter do
|
|
5
6
|
|
|
6
|
-
let(:
|
|
7
|
+
let(:env) { Rack::MockRequest.env_for("http://example.com:8080/") }
|
|
8
|
+
let(:request) { Rack::Request.new(env) }
|
|
9
|
+
let(:response) { Rack::MockResponse.new(200, {}, "") }
|
|
10
|
+
let(:context) { double(request: request, response: response) }
|
|
7
11
|
subject { Split::Persistence::CookieAdapter.new(context) }
|
|
8
12
|
|
|
9
13
|
describe "#[] and #[]=" do
|
|
@@ -30,7 +34,7 @@ describe Split::Persistence::CookieAdapter do
|
|
|
30
34
|
end
|
|
31
35
|
|
|
32
36
|
it "handles invalid JSON" do
|
|
33
|
-
context.cookies[:split] = { :value => '{"foo":2,', :expires => Time.now }
|
|
37
|
+
context.request.cookies[:split] = { :value => '{"foo":2,', :expires => Time.now }
|
|
34
38
|
expect(subject["my_key"]).to be_nil
|
|
35
39
|
subject["my_key"] = "my_value"
|
|
36
40
|
expect(subject["my_key"]).to eq("my_value")
|
|
@@ -47,7 +47,7 @@ describe Split::Persistence::DualAdapter do
|
|
|
47
47
|
context "when logged in" do
|
|
48
48
|
subject {
|
|
49
49
|
described_class.with_config(
|
|
50
|
-
logged_in:
|
|
50
|
+
logged_in: lambda { |context| true },
|
|
51
51
|
logged_in_adapter: selected_adapter,
|
|
52
52
|
logged_out_adapter: not_selected_adapter
|
|
53
53
|
).new(context)
|
|
@@ -59,7 +59,7 @@ describe Split::Persistence::DualAdapter do
|
|
|
59
59
|
context "when not logged in" do
|
|
60
60
|
subject {
|
|
61
61
|
described_class.with_config(
|
|
62
|
-
logged_in:
|
|
62
|
+
logged_in: lambda { |context| false },
|
|
63
63
|
logged_in_adapter: not_selected_adapter,
|
|
64
64
|
logged_out_adapter: selected_adapter
|
|
65
65
|
).new(context)
|
data/split.gemspec
CHANGED
|
@@ -12,7 +12,17 @@ Gem::Specification.new do |s|
|
|
|
12
12
|
s.homepage = "https://github.com/splitrb/split"
|
|
13
13
|
s.summary = "Rack based split testing framework"
|
|
14
14
|
|
|
15
|
-
s.
|
|
15
|
+
s.metadata = {
|
|
16
|
+
"homepage_uri" => "https://github.com/splitrb/split",
|
|
17
|
+
"changelog_uri" => "https://github.com/splitrb/split/blob/master/CHANGELOG.md",
|
|
18
|
+
"source_code_uri" => "https://github.com/splitrb/split",
|
|
19
|
+
"bug_tracker_uri" => "https://github.com/splitrb/split/issues",
|
|
20
|
+
"wiki_uri" => "https://github.com/splitrb/split/wiki",
|
|
21
|
+
"mailing_list_uri" => "https://groups.google.com/d/forum/split-ruby"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
s.required_ruby_version = '>= 1.9.3'
|
|
25
|
+
s.required_rubygems_version = '>= 2.0.0'
|
|
16
26
|
|
|
17
27
|
s.rubyforge_project = "split"
|
|
18
28
|
|
|
@@ -24,10 +34,10 @@ Gem::Specification.new do |s|
|
|
|
24
34
|
s.add_dependency 'sinatra', '>= 1.2.6'
|
|
25
35
|
s.add_dependency 'simple-random', '>= 0.9.3'
|
|
26
36
|
|
|
27
|
-
s.add_development_dependency 'bundler', '~> 1.
|
|
37
|
+
s.add_development_dependency 'bundler', '~> 1.14'
|
|
28
38
|
s.add_development_dependency 'simplecov', '~> 0.12'
|
|
29
39
|
s.add_development_dependency 'rack-test', '~> 0.6'
|
|
30
|
-
s.add_development_dependency 'rake', '~>
|
|
40
|
+
s.add_development_dependency 'rake', '~> 12'
|
|
31
41
|
s.add_development_dependency 'rspec', '~> 3.4'
|
|
32
42
|
s.add_development_dependency 'pry', '~> 0.10'
|
|
33
43
|
s.add_development_dependency 'fakeredis', '~> 0.6.0'
|