split 4.0.0.pre2 → 4.0.2

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 (74) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +14 -1
  3. data/.rubocop.yml +2 -5
  4. data/CHANGELOG.md +26 -2
  5. data/CONTRIBUTING.md +1 -1
  6. data/Gemfile +2 -1
  7. data/README.md +4 -2
  8. data/Rakefile +4 -5
  9. data/gemfiles/5.2.gemfile +1 -3
  10. data/gemfiles/6.0.gemfile +1 -3
  11. data/gemfiles/{5.0.gemfile → 6.1.gemfile} +2 -4
  12. data/gemfiles/{5.1.gemfile → 7.0.gemfile} +3 -4
  13. data/lib/split/algorithms/block_randomization.rb +5 -6
  14. data/lib/split/algorithms/whiplash.rb +16 -18
  15. data/lib/split/algorithms.rb +22 -0
  16. data/lib/split/alternative.rb +21 -22
  17. data/lib/split/cache.rb +0 -1
  18. data/lib/split/combined_experiments_helper.rb +4 -4
  19. data/lib/split/configuration.rb +83 -84
  20. data/lib/split/dashboard/helpers.rb +6 -7
  21. data/lib/split/dashboard/pagination_helpers.rb +53 -54
  22. data/lib/split/dashboard/public/style.css +5 -2
  23. data/lib/split/dashboard/views/index.erb +19 -4
  24. data/lib/split/dashboard.rb +29 -23
  25. data/lib/split/encapsulated_helper.rb +4 -6
  26. data/lib/split/experiment.rb +84 -88
  27. data/lib/split/experiment_catalog.rb +6 -5
  28. data/lib/split/extensions/string.rb +1 -1
  29. data/lib/split/goals_collection.rb +8 -10
  30. data/lib/split/helper.rb +19 -19
  31. data/lib/split/metric.rb +4 -5
  32. data/lib/split/persistence/cookie_adapter.rb +44 -47
  33. data/lib/split/persistence/dual_adapter.rb +7 -8
  34. data/lib/split/persistence/redis_adapter.rb +2 -3
  35. data/lib/split/persistence/session_adapter.rb +0 -2
  36. data/lib/split/persistence.rb +4 -4
  37. data/lib/split/redis_interface.rb +1 -2
  38. data/lib/split/trial.rb +23 -24
  39. data/lib/split/user.rb +12 -13
  40. data/lib/split/version.rb +1 -1
  41. data/lib/split/zscore.rb +1 -3
  42. data/lib/split.rb +26 -25
  43. data/spec/algorithms/block_randomization_spec.rb +6 -5
  44. data/spec/algorithms/weighted_sample_spec.rb +6 -5
  45. data/spec/algorithms/whiplash_spec.rb +4 -5
  46. data/spec/alternative_spec.rb +35 -36
  47. data/spec/cache_spec.rb +15 -19
  48. data/spec/combined_experiments_helper_spec.rb +18 -17
  49. data/spec/configuration_spec.rb +32 -38
  50. data/spec/dashboard/pagination_helpers_spec.rb +69 -67
  51. data/spec/dashboard/paginator_spec.rb +10 -9
  52. data/spec/dashboard_helpers_spec.rb +19 -18
  53. data/spec/dashboard_spec.rb +67 -35
  54. data/spec/encapsulated_helper_spec.rb +12 -14
  55. data/spec/experiment_catalog_spec.rb +14 -13
  56. data/spec/experiment_spec.rb +121 -123
  57. data/spec/goals_collection_spec.rb +17 -15
  58. data/spec/helper_spec.rb +379 -382
  59. data/spec/metric_spec.rb +14 -14
  60. data/spec/persistence/cookie_adapter_spec.rb +23 -8
  61. data/spec/persistence/dual_adapter_spec.rb +71 -71
  62. data/spec/persistence/redis_adapter_spec.rb +25 -26
  63. data/spec/persistence/session_adapter_spec.rb +2 -3
  64. data/spec/persistence_spec.rb +1 -2
  65. data/spec/redis_interface_spec.rb +16 -14
  66. data/spec/spec_helper.rb +15 -13
  67. data/spec/split_spec.rb +11 -11
  68. data/spec/support/cookies_mock.rb +1 -2
  69. data/spec/trial_spec.rb +61 -60
  70. data/spec/user_spec.rb +36 -36
  71. data/split.gemspec +20 -20
  72. metadata +9 -10
  73. data/.rubocop_todo.yml +0 -226
  74. data/Appraisals +0 -19
@@ -1,7 +1,8 @@
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
@@ -9,8 +10,8 @@ describe Split::Dashboard do
9
10
  class TestDashboard < Split::Dashboard
10
11
  include Split::Helper
11
12
 
12
- get '/my_experiment' do
13
- ab_test(params[:experiment], 'blue', 'red')
13
+ get "/my_experiment" do
14
+ ab_test(params[:experiment], "blue", "red")
14
15
  end
15
16
  end
16
17
 
@@ -27,11 +28,11 @@ describe Split::Dashboard do
27
28
  }
28
29
 
29
30
  let(:experiment_with_goals) {
30
- 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")
31
32
  }
32
33
 
33
34
  let(:metric) {
34
- 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])
35
36
  }
36
37
 
37
38
  let(:red_link) { link("red") }
@@ -42,7 +43,7 @@ describe Split::Dashboard do
42
43
  end
43
44
 
44
45
  it "should respond to /" do
45
- get '/'
46
+ get "/"
46
47
  expect(last_response).to be_ok
47
48
  end
48
49
 
@@ -54,33 +55,33 @@ describe Split::Dashboard do
54
55
  context "experiment without goals" do
55
56
  it "should display a Start button" do
56
57
  experiment
57
- get '/'
58
- expect(last_response.body).to include('Start')
58
+ get "/"
59
+ expect(last_response.body).to include("Start")
59
60
 
60
61
  post "/start?experiment=#{experiment.name}"
61
- get '/'
62
- expect(last_response.body).to include('Reset Data')
63
- 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:")
64
65
  end
65
66
  end
66
67
 
67
68
  context "experiment with metrics" do
68
69
  it "should display the names of associated metrics" do
69
70
  metric
70
- get '/'
71
- expect(last_response.body).to include('Metrics:testmetric')
71
+ get "/"
72
+ expect(last_response.body).to include("Metrics:testmetric")
72
73
  end
73
74
  end
74
75
 
75
76
  context "with goals" do
76
77
  it "should display a Start button" do
77
78
  experiment_with_goals
78
- get '/'
79
- expect(last_response.body).to include('Start')
79
+ get "/"
80
+ expect(last_response.body).to include("Start")
80
81
 
81
82
  post "/start?experiment=#{experiment.name}"
82
- get '/'
83
- expect(last_response.body).to include('Reset Data')
83
+ get "/"
84
+ expect(last_response.body).to include("Reset Data")
84
85
  end
85
86
  end
86
87
  end
@@ -88,7 +89,7 @@ describe Split::Dashboard do
88
89
  describe "force alternative" do
89
90
  context "initial version" do
90
91
  let!(:user) do
91
- Split::User.new(@app, { experiment.name => 'red' })
92
+ Split::User.new(@app, { experiment.name => "red" })
92
93
  end
93
94
 
94
95
  before do
@@ -115,7 +116,7 @@ describe Split::Dashboard do
115
116
  context "incremented version" do
116
117
  let!(:user) do
117
118
  experiment.increment_version
118
- Split::User.new(@app, { "#{experiment.name}:#{experiment.version}" => 'red' })
119
+ Split::User.new(@app, { "#{experiment.name}:#{experiment.version}" => "red" })
119
120
  end
120
121
 
121
122
  before do
@@ -134,28 +135,28 @@ describe Split::Dashboard do
134
135
 
135
136
  describe "index page" do
136
137
  context "with winner" do
137
- before { experiment.winner = 'red' }
138
+ before { experiment.winner = "red" }
138
139
 
139
140
  it "displays `Reopen Experiment` button" do
140
- get '/'
141
+ get "/"
141
142
 
142
- expect(last_response.body).to include('Reopen Experiment')
143
+ expect(last_response.body).to include("Reopen Experiment")
143
144
  end
144
145
  end
145
146
 
146
147
  context "without winner" do
147
148
  it "should not display `Reopen Experiment` button" do
148
- get '/'
149
+ get "/"
149
150
 
150
- expect(last_response.body).to_not include('Reopen Experiment')
151
+ expect(last_response.body).to_not include("Reopen Experiment")
151
152
  end
152
153
  end
153
154
  end
154
155
 
155
156
  describe "reopen experiment" do
156
- before { experiment.winner = 'red' }
157
+ before { experiment.winner = "red" }
157
158
 
158
- it 'redirects' do
159
+ it "redirects" do
159
160
  post "/reopen?experiment=#{experiment.name}"
160
161
 
161
162
  expect(last_response).to be_redirect
@@ -170,7 +171,7 @@ describe Split::Dashboard do
170
171
  it "keeps existing stats" do
171
172
  red_link.participant_count = 5
172
173
  blue_link.participant_count = 7
173
- experiment.winner = 'blue'
174
+ experiment.winner = "blue"
174
175
 
175
176
  post "/reopen?experiment=#{experiment.name}"
176
177
 
@@ -201,10 +202,41 @@ describe Split::Dashboard do
201
202
  end
202
203
  end
203
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
+
204
236
  it "should reset an experiment" do
205
237
  red_link.participant_count = 5
206
238
  blue_link.participant_count = 7
207
- experiment.winner = 'blue'
239
+ experiment.winner = "blue"
208
240
 
209
241
  post "/reset?experiment=#{experiment.name}"
210
242
 
@@ -226,16 +258,16 @@ describe Split::Dashboard do
226
258
 
227
259
  it "should mark an alternative as the winner" do
228
260
  expect(experiment.winner).to be_nil
229
- post "/experiment?experiment=#{experiment.name}", :alternative => 'red'
261
+ post "/experiment?experiment=#{experiment.name}", alternative: "red"
230
262
 
231
263
  expect(last_response).to be_redirect
232
- expect(experiment.winner.name).to eq('red')
264
+ expect(experiment.winner.name).to eq("red")
233
265
  end
234
266
 
235
267
  it "should display the start date" do
236
268
  experiment.start
237
269
 
238
- get '/'
270
+ get "/"
239
271
 
240
272
  expect(last_response.body).to include("<small>#{experiment.start_time.strftime('%Y-%m-%d')}</small>")
241
273
  end
@@ -243,8 +275,8 @@ describe Split::Dashboard do
243
275
  it "should handle experiments without a start date" do
244
276
  Split.redis.hdel(:experiment_start_times, experiment.name)
245
277
 
246
- get '/'
278
+ get "/"
247
279
 
248
- expect(last_response.body).to include('<small>Unknown</small>')
280
+ expect(last_response.body).to include("<small>Unknown</small>")
249
281
  end
250
282
  end
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
- require 'spec_helper'
2
+
3
+ require "spec_helper"
3
4
 
4
5
  describe Split::EncapsulatedHelper do
5
6
  include Split::EncapsulatedHelper
6
7
 
7
-
8
8
  def params
9
- raise NoMethodError, 'This method is not really defined'
9
+ raise NoMethodError, "This method is not really defined"
10
10
  end
11
11
 
12
12
  describe "ab_test" do
@@ -16,37 +16,35 @@ describe Split::EncapsulatedHelper do
16
16
  end
17
17
 
18
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
19
+ expect { params }.to raise_error(NoMethodError)
20
+ expect { ab_test("link_color", "blue", "red") }.not_to raise_error
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
28
-
29
28
  it "works inside ERB" do
30
- require 'erb'
31
- template = ERB.new(<<-ERB.split(/\s+/s).map(&:strip).join(' '), nil, "%")
29
+ require "erb"
30
+ template = ERB.new(<<-ERB.split(/\s+/s).map(&:strip).join(" "), nil, "%")
32
31
  foo <% ab_test(:foo, '1', '2') do |alt, meta| %>
33
32
  static <%= alt %>
34
33
  <% end %>
35
34
  ERB
36
35
  expect(template.result(binding)).to match(/foo static \d/)
37
36
  end
38
-
39
37
  end
40
38
  end
41
39
 
42
40
  describe "context" do
43
- it 'is passed in shim' do
44
- ctx = Class.new{
41
+ it "is passed in shim" do
42
+ ctx = Class.new {
45
43
  include Split::EncapsulatedHelper
46
44
  public :session
47
45
  }.new
48
- expect(ctx).to receive(:session){{}}
49
- expect{ ctx.ab_test('link_color', 'blue', 'red') }.not_to raise_error
46
+ expect(ctx).to receive(:session) { {} }
47
+ expect { ctx.ab_test("link_color", "blue", "red") }.not_to raise_error
50
48
  end
51
49
  end
52
50
  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