split 3.4.1 → 4.0.4

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.
Files changed (85) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/dependabot.yml +7 -0
  4. data/.github/workflows/ci.yml +76 -0
  5. data/.rubocop.yml +177 -4
  6. data/CHANGELOG.md +87 -0
  7. data/CONTRIBUTING.md +1 -1
  8. data/Gemfile +2 -1
  9. data/README.md +37 -9
  10. data/Rakefile +5 -5
  11. data/gemfiles/5.2.gemfile +1 -3
  12. data/gemfiles/6.0.gemfile +1 -3
  13. data/gemfiles/{5.0.gemfile → 6.1.gemfile} +2 -4
  14. data/gemfiles/{5.1.gemfile → 7.0.gemfile} +2 -4
  15. data/lib/split/algorithms/block_randomization.rb +6 -6
  16. data/lib/split/algorithms/weighted_sample.rb +2 -1
  17. data/lib/split/algorithms/whiplash.rb +17 -18
  18. data/lib/split/algorithms.rb +14 -0
  19. data/lib/split/alternative.rb +22 -22
  20. data/lib/split/cache.rb +27 -0
  21. data/lib/split/combined_experiments_helper.rb +5 -4
  22. data/lib/split/configuration.rb +89 -94
  23. data/lib/split/dashboard/helpers.rb +7 -7
  24. data/lib/split/dashboard/pagination_helpers.rb +54 -54
  25. data/lib/split/dashboard/paginator.rb +1 -0
  26. data/lib/split/dashboard/public/dashboard.js +10 -0
  27. data/lib/split/dashboard/public/style.css +10 -2
  28. data/lib/split/dashboard/views/_controls.erb +13 -0
  29. data/lib/split/dashboard/views/_experiment.erb +2 -1
  30. data/lib/split/dashboard/views/index.erb +19 -4
  31. data/lib/split/dashboard.rb +42 -21
  32. data/lib/split/encapsulated_helper.rb +15 -8
  33. data/lib/split/engine.rb +1 -0
  34. data/lib/split/exceptions.rb +1 -0
  35. data/lib/split/experiment.rb +151 -124
  36. data/lib/split/experiment_catalog.rb +7 -8
  37. data/lib/split/extensions/string.rb +2 -1
  38. data/lib/split/goals_collection.rb +9 -10
  39. data/lib/split/helper.rb +50 -23
  40. data/lib/split/metric.rb +6 -6
  41. data/lib/split/persistence/cookie_adapter.rb +46 -44
  42. data/lib/split/persistence/dual_adapter.rb +7 -8
  43. data/lib/split/persistence/redis_adapter.rb +8 -4
  44. data/lib/split/persistence/session_adapter.rb +1 -2
  45. data/lib/split/persistence.rb +8 -6
  46. data/lib/split/redis_interface.rb +15 -29
  47. data/lib/split/trial.rb +43 -34
  48. data/lib/split/user.rb +25 -14
  49. data/lib/split/version.rb +2 -4
  50. data/lib/split/zscore.rb +2 -3
  51. data/lib/split.rb +34 -27
  52. data/spec/algorithms/block_randomization_spec.rb +6 -5
  53. data/spec/algorithms/weighted_sample_spec.rb +6 -5
  54. data/spec/algorithms/whiplash_spec.rb +4 -5
  55. data/spec/alternative_spec.rb +35 -36
  56. data/spec/cache_spec.rb +84 -0
  57. data/spec/combined_experiments_helper_spec.rb +18 -17
  58. data/spec/configuration_spec.rb +41 -45
  59. data/spec/dashboard/pagination_helpers_spec.rb +69 -67
  60. data/spec/dashboard/paginator_spec.rb +10 -9
  61. data/spec/dashboard_helpers_spec.rb +19 -18
  62. data/spec/dashboard_spec.rb +122 -38
  63. data/spec/encapsulated_helper_spec.rb +46 -22
  64. data/spec/experiment_catalog_spec.rb +14 -13
  65. data/spec/experiment_spec.rb +198 -118
  66. data/spec/goals_collection_spec.rb +18 -16
  67. data/spec/helper_spec.rb +454 -385
  68. data/spec/metric_spec.rb +14 -14
  69. data/spec/persistence/cookie_adapter_spec.rb +26 -11
  70. data/spec/persistence/dual_adapter_spec.rb +71 -71
  71. data/spec/persistence/redis_adapter_spec.rb +35 -27
  72. data/spec/persistence/session_adapter_spec.rb +2 -3
  73. data/spec/persistence_spec.rb +1 -2
  74. data/spec/redis_interface_spec.rb +25 -82
  75. data/spec/spec_helper.rb +35 -24
  76. data/spec/split_spec.rb +11 -11
  77. data/spec/support/cookies_mock.rb +1 -2
  78. data/spec/trial_spec.rb +102 -75
  79. data/spec/user_spec.rb +60 -29
  80. data/split.gemspec +22 -21
  81. metadata +43 -40
  82. data/.rubocop_todo.yml +0 -679
  83. data/.travis.yml +0 -60
  84. data/Appraisals +0 -19
  85. data/gemfiles/4.2.gemfile +0 -9
@@ -1,13 +1,22 @@
1
1
  # frozen_string_literal: true
2
- require 'spec_helper'
3
- require 'rack/test'
4
- require 'split/dashboard'
2
+
3
+ require "spec_helper"
4
+ require "rack/test"
5
+ require "split/dashboard"
5
6
 
6
7
  describe Split::Dashboard do
7
8
  include Rack::Test::Methods
8
9
 
10
+ class TestDashboard < Split::Dashboard
11
+ include Split::Helper
12
+
13
+ get "/my_experiment" do
14
+ ab_test(params[:experiment], "blue", "red")
15
+ end
16
+ end
17
+
9
18
  def app
10
- @app ||= Split::Dashboard
19
+ @app ||= TestDashboard
11
20
  end
12
21
 
13
22
  def link(color)
@@ -19,11 +28,11 @@ describe Split::Dashboard do
19
28
  }
20
29
 
21
30
  let(:experiment_with_goals) {
22
- Split::ExperimentCatalog.find_or_create({"link_color" => ["goal_1", "goal_2"]}, "blue", "red")
31
+ Split::ExperimentCatalog.find_or_create({ "link_color" => ["goal_1", "goal_2"] }, "blue", "red")
23
32
  }
24
33
 
25
34
  let(:metric) {
26
- Split::Metric.find_or_create(name: 'testmetric', experiments: [experiment, experiment_with_goals])
35
+ Split::Metric.find_or_create(name: "testmetric", experiments: [experiment, experiment_with_goals])
27
36
  }
28
37
 
29
38
  let(:red_link) { link("red") }
@@ -34,7 +43,7 @@ describe Split::Dashboard do
34
43
  end
35
44
 
36
45
  it "should respond to /" do
37
- get '/'
46
+ get "/"
38
47
  expect(last_response).to be_ok
39
48
  end
40
49
 
@@ -46,33 +55,33 @@ describe Split::Dashboard do
46
55
  context "experiment without goals" do
47
56
  it "should display a Start button" do
48
57
  experiment
49
- get '/'
50
- expect(last_response.body).to include('Start')
58
+ get "/"
59
+ expect(last_response.body).to include("Start")
51
60
 
52
61
  post "/start?experiment=#{experiment.name}"
53
- get '/'
54
- expect(last_response.body).to include('Reset Data')
55
- expect(last_response.body).not_to include('Metrics:')
62
+ get "/"
63
+ expect(last_response.body).to include("Reset Data")
64
+ expect(last_response.body).not_to include("Metrics:")
56
65
  end
57
66
  end
58
67
 
59
68
  context "experiment with metrics" do
60
69
  it "should display the names of associated metrics" do
61
70
  metric
62
- get '/'
63
- expect(last_response.body).to include('Metrics:testmetric')
71
+ get "/"
72
+ expect(last_response.body).to include("Metrics:testmetric")
64
73
  end
65
74
  end
66
75
 
67
76
  context "with goals" do
68
77
  it "should display a Start button" do
69
78
  experiment_with_goals
70
- get '/'
71
- expect(last_response.body).to include('Start')
79
+ get "/"
80
+ expect(last_response.body).to include("Start")
72
81
 
73
82
  post "/start?experiment=#{experiment.name}"
74
- get '/'
75
- expect(last_response.body).to include('Reset Data')
83
+ get "/"
84
+ expect(last_response.body).to include("Reset Data")
76
85
  end
77
86
  end
78
87
  end
@@ -80,7 +89,7 @@ describe Split::Dashboard do
80
89
  describe "force alternative" do
81
90
  context "initial version" do
82
91
  let!(:user) do
83
- Split::User.new(@app, { experiment.name => 'red' })
92
+ Split::User.new(@app, { experiment.name => "red" })
84
93
  end
85
94
 
86
95
  before do
@@ -90,15 +99,24 @@ describe Split::Dashboard do
90
99
  it "should set current user's alternative" do
91
100
  blue_link.participant_count = 7
92
101
  post "/force_alternative?experiment=#{experiment.name}", alternative: "blue"
93
- expect(user[experiment.key]).to eq("blue")
94
- expect(blue_link.participant_count).to eq(8)
102
+
103
+ get "/my_experiment?experiment=#{experiment.name}"
104
+ expect(last_response.body).to include("blue")
105
+ end
106
+
107
+ it "should not modify an existing user" do
108
+ blue_link.participant_count = 7
109
+ post "/force_alternative?experiment=#{experiment.name}", alternative: "blue"
110
+
111
+ expect(user[experiment.key]).to eq("red")
112
+ expect(blue_link.participant_count).to eq(7)
95
113
  end
96
114
  end
97
115
 
98
116
  context "incremented version" do
99
117
  let!(:user) do
100
118
  experiment.increment_version
101
- Split::User.new(@app, { "#{experiment.name}:#{experiment.version}" => 'red' })
119
+ Split::User.new(@app, { "#{experiment.name}:#{experiment.version}" => "red" })
102
120
  end
103
121
 
104
122
  before do
@@ -108,36 +126,37 @@ describe Split::Dashboard do
108
126
  it "should set current user's alternative" do
109
127
  blue_link.participant_count = 7
110
128
  post "/force_alternative?experiment=#{experiment.name}", alternative: "blue"
111
- expect(user[experiment.key]).to eq("blue")
112
- expect(blue_link.participant_count).to eq(8)
129
+
130
+ get "/my_experiment?experiment=#{experiment.name}"
131
+ expect(last_response.body).to include("blue")
113
132
  end
114
133
  end
115
134
  end
116
135
 
117
136
  describe "index page" do
118
137
  context "with winner" do
119
- before { experiment.winner = 'red' }
138
+ before { experiment.winner = "red" }
120
139
 
121
140
  it "displays `Reopen Experiment` button" do
122
- get '/'
141
+ get "/"
123
142
 
124
- expect(last_response.body).to include('Reopen Experiment')
143
+ expect(last_response.body).to include("Reopen Experiment")
125
144
  end
126
145
  end
127
146
 
128
147
  context "without winner" do
129
148
  it "should not display `Reopen Experiment` button" do
130
- get '/'
149
+ get "/"
131
150
 
132
- expect(last_response.body).to_not include('Reopen Experiment')
151
+ expect(last_response.body).to_not include("Reopen Experiment")
133
152
  end
134
153
  end
135
154
  end
136
155
 
137
156
  describe "reopen experiment" do
138
- before { experiment.winner = 'red' }
157
+ before { experiment.winner = "red" }
139
158
 
140
- it 'redirects' do
159
+ it "redirects" do
141
160
  post "/reopen?experiment=#{experiment.name}"
142
161
 
143
162
  expect(last_response).to be_redirect
@@ -152,7 +171,7 @@ describe Split::Dashboard do
152
171
  it "keeps existing stats" do
153
172
  red_link.participant_count = 5
154
173
  blue_link.participant_count = 7
155
- experiment.winner = 'blue'
174
+ experiment.winner = "blue"
156
175
 
157
176
  post "/reopen?experiment=#{experiment.name}"
158
177
 
@@ -161,10 +180,63 @@ describe Split::Dashboard do
161
180
  end
162
181
  end
163
182
 
183
+ describe "update cohorting" do
184
+ it "calls enable of cohorting when action is enable" do
185
+ post "/update_cohorting?experiment=#{experiment.name}", { "cohorting_action": "enable" }
186
+
187
+ expect(experiment.cohorting_disabled?).to eq false
188
+ end
189
+
190
+ it "calls disable of cohorting when action is disable" do
191
+ post "/update_cohorting?experiment=#{experiment.name}", { "cohorting_action": "disable" }
192
+
193
+ expect(experiment.cohorting_disabled?).to eq true
194
+ end
195
+
196
+ it "calls neither enable or disable cohorting when passed invalid action" do
197
+ previous_value = experiment.cohorting_disabled?
198
+
199
+ post "/update_cohorting?experiment=#{experiment.name}", { "cohorting_action": "other" }
200
+
201
+ expect(experiment.cohorting_disabled?).to eq previous_value
202
+ end
203
+ end
204
+
205
+ describe "initialize experiment" do
206
+ before do
207
+ Split.configuration.experiments = {
208
+ my_experiment: {
209
+ alternatives: [ "control", "alternative" ],
210
+ }
211
+ }
212
+ end
213
+
214
+ it "initializes the experiment when the experiment is given" do
215
+ expect(Split::ExperimentCatalog.find("my_experiment")).to be nil
216
+
217
+ post "/initialize_experiment", { experiment: "my_experiment" }
218
+
219
+ experiment = Split::ExperimentCatalog.find("my_experiment")
220
+ expect(experiment).to be_a(Split::Experiment)
221
+ end
222
+
223
+ it "does not attempt to intialize the experiment when empty experiment is given" do
224
+ post "/initialize_experiment", { experiment: "" }
225
+
226
+ expect(Split::ExperimentCatalog).to_not receive(:find_or_create)
227
+ end
228
+
229
+ it "does not attempt to intialize the experiment when no experiment is given" do
230
+ post "/initialize_experiment"
231
+
232
+ expect(Split::ExperimentCatalog).to_not receive(:find_or_create)
233
+ end
234
+ end
235
+
164
236
  it "should reset an experiment" do
165
237
  red_link.participant_count = 5
166
238
  blue_link.participant_count = 7
167
- experiment.winner = 'blue'
239
+ experiment.winner = "blue"
168
240
 
169
241
  post "/reset?experiment=#{experiment.name}"
170
242
 
@@ -186,16 +258,16 @@ describe Split::Dashboard do
186
258
 
187
259
  it "should mark an alternative as the winner" do
188
260
  expect(experiment.winner).to be_nil
189
- post "/experiment?experiment=#{experiment.name}", :alternative => 'red'
261
+ post "/experiment?experiment=#{experiment.name}", alternative: "red"
190
262
 
191
263
  expect(last_response).to be_redirect
192
- expect(experiment.winner.name).to eq('red')
264
+ expect(experiment.winner.name).to eq("red")
193
265
  end
194
266
 
195
267
  it "should display the start date" do
196
268
  experiment.start
197
269
 
198
- get '/'
270
+ get "/"
199
271
 
200
272
  expect(last_response.body).to include("<small>#{experiment.start_time.strftime('%Y-%m-%d')}</small>")
201
273
  end
@@ -203,8 +275,20 @@ describe Split::Dashboard do
203
275
  it "should handle experiments without a start date" do
204
276
  Split.redis.hdel(:experiment_start_times, experiment.name)
205
277
 
206
- get '/'
278
+ get "/"
279
+
280
+ expect(last_response.body).to include("<small>Unknown</small>")
281
+ end
282
+
283
+ it "should be explode with experiments with invalid data" do
284
+ red_link.participant_count = 1
285
+ red_link.set_completed_count(10)
286
+
287
+ blue_link.participant_count = 3
288
+ blue_link.set_completed_count(2)
207
289
 
208
- expect(last_response.body).to include('<small>Unknown</small>')
290
+ get "/"
291
+
292
+ expect(last_response).to be_ok
209
293
  end
210
294
  end
@@ -1,13 +1,9 @@
1
1
  # frozen_string_literal: true
2
- require 'spec_helper'
3
-
4
- describe Split::EncapsulatedHelper do
5
- include Split::EncapsulatedHelper
6
2
 
3
+ require "spec_helper"
7
4
 
8
- def params
9
- raise NoMethodError, 'This method is not really defined'
10
- end
5
+ describe Split::EncapsulatedHelper do
6
+ let(:context_shim) { Split::EncapsulatedHelper::ContextShim.new(double(request: request)) }
11
7
 
12
8
  describe "ab_test" do
13
9
  before do
@@ -15,38 +11,66 @@ describe Split::EncapsulatedHelper do
15
11
  .and_return(mock_user)
16
12
  end
17
13
 
18
- it "should not raise an error when params raises an error" do
19
- expect{ params }.to raise_error(NoMethodError)
20
- expect(lambda { ab_test('link_color', 'blue', 'red') }).not_to raise_error
21
- end
22
-
23
14
  it "calls the block with selected alternative" do
24
- expect{|block| ab_test('link_color', 'red', 'red', &block) }.to yield_with_args('red', nil)
15
+ expect { |block| context_shim.ab_test("link_color", "red", "red", &block) }.to yield_with_args("red", {})
25
16
  end
26
17
 
27
18
  context "inside a view" do
28
-
29
19
  it "works inside ERB" do
30
- require 'erb'
31
- template = ERB.new(<<-ERB.split(/\s+/s).map(&:strip).join(' '), nil, "%")
32
- foo <% ab_test(:foo, '1', '2') do |alt, meta| %>
20
+ require "erb"
21
+ template = ERB.new(<<-ERB.split(/\s+/s).map(&:strip).join(" "), nil, "%")
22
+ foo <% context_shim.ab_test(:foo, '1', '2') do |alt, meta| %>
33
23
  static <%= alt %>
34
24
  <% end %>
35
25
  ERB
36
26
  expect(template.result(binding)).to match(/foo static \d/)
37
27
  end
38
-
39
28
  end
40
29
  end
41
30
 
42
31
  describe "context" do
43
- it 'is passed in shim' do
44
- ctx = Class.new{
32
+ it "is passed in shim" do
33
+ ctx = Class.new {
45
34
  include Split::EncapsulatedHelper
46
35
  public :session
47
36
  }.new
48
- expect(ctx).to receive(:session){{}}
49
- expect{ ctx.ab_test('link_color', 'blue', 'red') }.not_to raise_error
37
+
38
+ expect(ctx).to receive(:session) { {} }
39
+ expect { ctx.ab_test("link_color", "blue", "red") }.not_to raise_error
40
+ end
41
+
42
+ context "when request is defined in context of ContextShim" do
43
+ context "when overriding by params" do
44
+ it do
45
+ ctx = Class.new {
46
+ public :session
47
+ def request
48
+ build_request(params: {
49
+ "ab_test" => { "link_color" => "blue" }
50
+ })
51
+ end
52
+ }.new
53
+
54
+ context_shim = Split::EncapsulatedHelper::ContextShim.new(ctx)
55
+ expect(context_shim.ab_test("link_color", "blue", "red")).to be("blue")
56
+ end
57
+ end
58
+
59
+ context "when overriding by cookies" do
60
+ it do
61
+ ctx = Class.new {
62
+ public :session
63
+ def request
64
+ build_request(cookies: {
65
+ "split_override" => '{ "link_color": "red" }'
66
+ })
67
+ end
68
+ }.new
69
+
70
+ context_shim = Split::EncapsulatedHelper::ContextShim.new(ctx)
71
+ expect(context_shim.ab_test("link_color", "blue", "red")).to be("red")
72
+ end
73
+ end
50
74
  end
51
75
  end
52
76
  end
@@ -1,53 +1,54 @@
1
1
  # frozen_string_literal: true
2
- require 'spec_helper'
2
+
3
+ require "spec_helper"
3
4
 
4
5
  describe Split::ExperimentCatalog do
5
6
  subject { Split::ExperimentCatalog }
6
7
 
7
8
  describe ".find_or_create" do
8
9
  it "should not raise an error when passed strings for alternatives" do
9
- expect { subject.find_or_create('xyz', '1', '2', '3') }.not_to raise_error
10
+ expect { subject.find_or_create("xyz", "1", "2", "3") }.not_to raise_error
10
11
  end
11
12
 
12
13
  it "should not raise an error when passed an array for alternatives" do
13
- expect { subject.find_or_create('xyz', ['1', '2', '3']) }.not_to raise_error
14
+ expect { subject.find_or_create("xyz", ["1", "2", "3"]) }.not_to raise_error
14
15
  end
15
16
 
16
17
  it "should raise the appropriate error when passed integers for alternatives" do
17
- expect { subject.find_or_create('xyz', 1, 2, 3) }.to raise_error(ArgumentError)
18
+ expect { subject.find_or_create("xyz", 1, 2, 3) }.to raise_error(ArgumentError)
18
19
  end
19
20
 
20
21
  it "should raise the appropriate error when passed symbols for alternatives" do
21
- expect { subject.find_or_create('xyz', :a, :b, :c) }.to raise_error(ArgumentError)
22
+ expect { subject.find_or_create("xyz", :a, :b, :c) }.to raise_error(ArgumentError)
22
23
  end
23
24
 
24
25
  it "should not raise error when passed an array for goals" do
25
- expect { subject.find_or_create({'link_color' => ["purchase", "refund"]}, 'blue', 'red') }
26
+ expect { subject.find_or_create({ "link_color" => ["purchase", "refund"] }, "blue", "red") }
26
27
  .not_to raise_error
27
28
  end
28
29
 
29
30
  it "should not raise error when passed just one goal" do
30
- expect { subject.find_or_create({'link_color' => "purchase"}, 'blue', 'red') }
31
+ expect { subject.find_or_create({ "link_color" => "purchase" }, "blue", "red") }
31
32
  .not_to raise_error
32
33
  end
33
34
 
34
35
  it "constructs a new experiment" do
35
- expect(subject.find_or_create('my_exp', 'control me').control.to_s).to eq('control me')
36
+ expect(subject.find_or_create("my_exp", "control me").control.to_s).to eq("control me")
36
37
  end
37
38
  end
38
39
 
39
- describe '.find' do
40
+ describe ".find" do
40
41
  it "should return an existing experiment" do
41
- experiment = Split::Experiment.new('basket_text', alternatives: ['blue', 'red', 'green'])
42
+ experiment = Split::Experiment.new("basket_text", alternatives: ["blue", "red", "green"])
42
43
  experiment.save
43
44
 
44
- experiment = subject.find('basket_text')
45
+ experiment = subject.find("basket_text")
45
46
 
46
- expect(experiment.name).to eq('basket_text')
47
+ expect(experiment.name).to eq("basket_text")
47
48
  end
48
49
 
49
50
  it "should return nil if experiment not exist" do
50
- expect(subject.find('non_existent_experiment')).to be_nil
51
+ expect(subject.find("non_existent_experiment")).to be_nil
51
52
  end
52
53
  end
53
54
  end