ab-split 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (93) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +30 -0
  3. data/.csslintrc +2 -0
  4. data/.eslintignore +1 -0
  5. data/.eslintrc +213 -0
  6. data/.github/FUNDING.yml +1 -0
  7. data/.github/ISSUE_TEMPLATE/bug_report.md +24 -0
  8. data/.rspec +1 -0
  9. data/.rubocop.yml +7 -0
  10. data/.rubocop_todo.yml +679 -0
  11. data/.travis.yml +60 -0
  12. data/Appraisals +19 -0
  13. data/CHANGELOG.md +696 -0
  14. data/CODE_OF_CONDUCT.md +74 -0
  15. data/CONTRIBUTING.md +62 -0
  16. data/Gemfile +7 -0
  17. data/LICENSE +22 -0
  18. data/README.md +955 -0
  19. data/Rakefile +9 -0
  20. data/ab-split.gemspec +44 -0
  21. data/gemfiles/4.2.gemfile +9 -0
  22. data/gemfiles/5.0.gemfile +9 -0
  23. data/gemfiles/5.1.gemfile +9 -0
  24. data/gemfiles/5.2.gemfile +9 -0
  25. data/gemfiles/6.0.gemfile +9 -0
  26. data/lib/split.rb +76 -0
  27. data/lib/split/algorithms/block_randomization.rb +23 -0
  28. data/lib/split/algorithms/weighted_sample.rb +18 -0
  29. data/lib/split/algorithms/whiplash.rb +38 -0
  30. data/lib/split/alternative.rb +191 -0
  31. data/lib/split/combined_experiments_helper.rb +37 -0
  32. data/lib/split/configuration.rb +255 -0
  33. data/lib/split/dashboard.rb +74 -0
  34. data/lib/split/dashboard/helpers.rb +45 -0
  35. data/lib/split/dashboard/pagination_helpers.rb +86 -0
  36. data/lib/split/dashboard/paginator.rb +16 -0
  37. data/lib/split/dashboard/public/dashboard-filtering.js +43 -0
  38. data/lib/split/dashboard/public/dashboard.js +24 -0
  39. data/lib/split/dashboard/public/jquery-1.11.1.min.js +4 -0
  40. data/lib/split/dashboard/public/reset.css +48 -0
  41. data/lib/split/dashboard/public/style.css +328 -0
  42. data/lib/split/dashboard/views/_controls.erb +18 -0
  43. data/lib/split/dashboard/views/_experiment.erb +155 -0
  44. data/lib/split/dashboard/views/_experiment_with_goal_header.erb +8 -0
  45. data/lib/split/dashboard/views/index.erb +26 -0
  46. data/lib/split/dashboard/views/layout.erb +27 -0
  47. data/lib/split/encapsulated_helper.rb +42 -0
  48. data/lib/split/engine.rb +15 -0
  49. data/lib/split/exceptions.rb +6 -0
  50. data/lib/split/experiment.rb +486 -0
  51. data/lib/split/experiment_catalog.rb +51 -0
  52. data/lib/split/extensions/string.rb +16 -0
  53. data/lib/split/goals_collection.rb +45 -0
  54. data/lib/split/helper.rb +165 -0
  55. data/lib/split/metric.rb +101 -0
  56. data/lib/split/persistence.rb +28 -0
  57. data/lib/split/persistence/cookie_adapter.rb +94 -0
  58. data/lib/split/persistence/dual_adapter.rb +85 -0
  59. data/lib/split/persistence/redis_adapter.rb +57 -0
  60. data/lib/split/persistence/session_adapter.rb +29 -0
  61. data/lib/split/redis_interface.rb +50 -0
  62. data/lib/split/trial.rb +117 -0
  63. data/lib/split/user.rb +69 -0
  64. data/lib/split/version.rb +7 -0
  65. data/lib/split/zscore.rb +57 -0
  66. data/spec/algorithms/block_randomization_spec.rb +32 -0
  67. data/spec/algorithms/weighted_sample_spec.rb +19 -0
  68. data/spec/algorithms/whiplash_spec.rb +24 -0
  69. data/spec/alternative_spec.rb +320 -0
  70. data/spec/combined_experiments_helper_spec.rb +57 -0
  71. data/spec/configuration_spec.rb +258 -0
  72. data/spec/dashboard/pagination_helpers_spec.rb +200 -0
  73. data/spec/dashboard/paginator_spec.rb +37 -0
  74. data/spec/dashboard_helpers_spec.rb +42 -0
  75. data/spec/dashboard_spec.rb +210 -0
  76. data/spec/encapsulated_helper_spec.rb +52 -0
  77. data/spec/experiment_catalog_spec.rb +53 -0
  78. data/spec/experiment_spec.rb +533 -0
  79. data/spec/goals_collection_spec.rb +80 -0
  80. data/spec/helper_spec.rb +1111 -0
  81. data/spec/metric_spec.rb +31 -0
  82. data/spec/persistence/cookie_adapter_spec.rb +106 -0
  83. data/spec/persistence/dual_adapter_spec.rb +194 -0
  84. data/spec/persistence/redis_adapter_spec.rb +90 -0
  85. data/spec/persistence/session_adapter_spec.rb +32 -0
  86. data/spec/persistence_spec.rb +34 -0
  87. data/spec/redis_interface_spec.rb +111 -0
  88. data/spec/spec_helper.rb +52 -0
  89. data/spec/split_spec.rb +43 -0
  90. data/spec/support/cookies_mock.rb +20 -0
  91. data/spec/trial_spec.rb +299 -0
  92. data/spec/user_spec.rb +87 -0
  93. metadata +322 -0
@@ -0,0 +1,258 @@
1
+ # frozen_string_literal: true
2
+ require 'spec_helper'
3
+
4
+ describe Split::Configuration do
5
+
6
+ before(:each) { @config = Split::Configuration.new }
7
+
8
+ it "should provide a default value for ignore_ip_addresses" do
9
+ expect(@config.ignore_ip_addresses).to eq([])
10
+ end
11
+
12
+ it "should provide default values for db failover" do
13
+ expect(@config.db_failover).to be_falsey
14
+ expect(@config.db_failover_on_db_error).to be_a Proc
15
+ end
16
+
17
+ it "should not allow multiple experiments by default" do
18
+ expect(@config.allow_multiple_experiments).to be_falsey
19
+ end
20
+
21
+ it "should be enabled by default" do
22
+ expect(@config.enabled).to be_truthy
23
+ end
24
+
25
+ it "disabled is the opposite of enabled" do
26
+ @config.enabled = false
27
+ expect(@config.disabled?).to be_truthy
28
+ end
29
+
30
+ it "should not store the overridden test group per default" do
31
+ expect(@config.store_override).to be_falsey
32
+ end
33
+
34
+ it "should provide a default pattern for robots" do
35
+ %w[Baidu Gigabot Googlebot libwww-perl lwp-trivial msnbot SiteUptime Slurp WordPress ZIBB ZyBorg YandexBot AdsBot-Google Wget curl bitlybot facebookexternalhit spider].each do |robot|
36
+ expect(@config.robot_regex).to match(robot)
37
+ end
38
+
39
+ expect(@config.robot_regex).to match("EventMachine HttpClient")
40
+ expect(@config.robot_regex).to match("libwww-perl/5.836")
41
+ expect(@config.robot_regex).to match("Pingdom.com_bot_version_1.4_(http://www.pingdom.com)")
42
+
43
+ expect(@config.robot_regex).to match(" - ")
44
+ end
45
+
46
+ it "should accept real UAs with the robot regexp" do
47
+ expect(@config.robot_regex).not_to match("Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.1.4) Gecko/20091017 SeaMonkey/2.0")
48
+ expect(@config.robot_regex).not_to match("Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; F-6.0SP2-20041109; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022; .NET CLR 1.1.4322; InfoPath.3)")
49
+ end
50
+
51
+ it "should allow adding a bot to the bot list" do
52
+ @config.bots["newbot"] = "An amazing test bot"
53
+ expect(@config.robot_regex).to match("newbot")
54
+ end
55
+
56
+ it "should use the session adapter for persistence by default" do
57
+ expect(@config.persistence).to eq(Split::Persistence::SessionAdapter)
58
+ end
59
+
60
+ it "should load a metric" do
61
+ @config.experiments = {:my_experiment=>
62
+ {:alternatives=>["control_opt", "other_opt"], :metric=>:my_metric}}
63
+
64
+ expect(@config.metrics).not_to be_nil
65
+ expect(@config.metrics.keys).to eq([:my_metric])
66
+ end
67
+
68
+ it "should allow loading of experiment using experment_for" do
69
+ @config.experiments = {:my_experiment=>
70
+ {:alternatives=>["control_opt", "other_opt"], :metric=>:my_metric}}
71
+ expect(@config.experiment_for(:my_experiment)).to eq({:alternatives=>["control_opt", ["other_opt"]]})
72
+ end
73
+
74
+ context "when experiments are defined via YAML" do
75
+ context "as strings" do
76
+ context "in a basic configuration" do
77
+ before do
78
+ experiments_yaml = <<-eos
79
+ my_experiment:
80
+ alternatives:
81
+ - Control Opt
82
+ - Alt One
83
+ - Alt Two
84
+ resettable: false
85
+ eos
86
+ @config.experiments = YAML.load(experiments_yaml)
87
+ end
88
+
89
+ it 'should normalize experiments' do
90
+ expect(@config.normalized_experiments).to eq({:my_experiment=>{:resettable=>false,:alternatives=>["Control Opt", ["Alt One", "Alt Two"]]}})
91
+ end
92
+ end
93
+
94
+ context "in a configuration with metadata" do
95
+ before do
96
+ experiments_yaml = <<-eos
97
+ my_experiment:
98
+ alternatives:
99
+ - name: Control Opt
100
+ percent: 67
101
+ - name: Alt One
102
+ percent: 10
103
+ - name: Alt Two
104
+ percent: 23
105
+ metadata:
106
+ Control Opt:
107
+ text: 'Control Option'
108
+ Alt One:
109
+ text: 'Alternative One'
110
+ Alt Two:
111
+ text: 'Alternative Two'
112
+ resettable: false
113
+ eos
114
+ @config.experiments = YAML.load(experiments_yaml)
115
+ end
116
+
117
+ it 'should have metadata on the experiment' do
118
+ meta = @config.normalized_experiments[:my_experiment][:metadata]
119
+ expect(meta).to_not be nil
120
+ expect(meta['Control Opt']['text']).to eq('Control Option')
121
+ end
122
+ end
123
+
124
+ context "in a complex configuration" do
125
+ before do
126
+ experiments_yaml = <<-eos
127
+ my_experiment:
128
+ alternatives:
129
+ - name: Control Opt
130
+ percent: 67
131
+ - name: Alt One
132
+ percent: 10
133
+ - name: Alt Two
134
+ percent: 23
135
+ resettable: false
136
+ metric: my_metric
137
+ another_experiment:
138
+ alternatives:
139
+ - a
140
+ - b
141
+ eos
142
+ @config.experiments = YAML.load(experiments_yaml)
143
+ end
144
+
145
+ it "should normalize experiments" do
146
+ expect(@config.normalized_experiments).to eq({:my_experiment=>{:resettable=>false,:alternatives=>[{"Control Opt"=>0.67},
147
+ [{"Alt One"=>0.1}, {"Alt Two"=>0.23}]]}, :another_experiment=>{:alternatives=>["a", ["b"]]}})
148
+ end
149
+
150
+ it "should recognize metrics" do
151
+ expect(@config.metrics).not_to be_nil
152
+ expect(@config.metrics.keys).to eq([:my_metric])
153
+ end
154
+
155
+ end
156
+ end
157
+
158
+ context "as symbols" do
159
+
160
+ context "with valid YAML" do
161
+ before do
162
+ experiments_yaml = <<-eos
163
+ :my_experiment:
164
+ :alternatives:
165
+ - Control Opt
166
+ - Alt One
167
+ - Alt Two
168
+ :resettable: false
169
+ eos
170
+ @config.experiments = YAML.load(experiments_yaml)
171
+ end
172
+
173
+ it "should normalize experiments" do
174
+ expect(@config.normalized_experiments).to eq({:my_experiment=>{:resettable=>false,:alternatives=>["Control Opt", ["Alt One", "Alt Two"]]}})
175
+ end
176
+ end
177
+
178
+ context "with invalid YAML" do
179
+
180
+ let(:yaml) { YAML.load(input) }
181
+
182
+ context "with an empty string" do
183
+ let(:input) { '' }
184
+
185
+ it "should raise an error" do
186
+ expect { @config.experiments = yaml }.to raise_error(Split::InvalidExperimentsFormatError)
187
+ end
188
+ end
189
+
190
+ context "with just the YAML header" do
191
+ let(:input) { '---' }
192
+
193
+ it "should raise an error" do
194
+ expect { @config.experiments = yaml }.to raise_error(Split::InvalidExperimentsFormatError)
195
+ end
196
+ end
197
+ end
198
+ end
199
+ end
200
+
201
+ it "should normalize experiments" do
202
+ @config.experiments = {
203
+ :my_experiment => {
204
+ :alternatives => [
205
+ { :name => "control_opt", :percent => 67 },
206
+ { :name => "second_opt", :percent => 10 },
207
+ { :name => "third_opt", :percent => 23 },
208
+ ],
209
+ }
210
+ }
211
+
212
+ expect(@config.normalized_experiments).to eq({:my_experiment=>{:alternatives=>[{"control_opt"=>0.67}, [{"second_opt"=>0.1}, {"third_opt"=>0.23}]]}})
213
+ end
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
+ context "redis configuration" do
230
+ it "should default to local redis server" do
231
+ expect(@config.redis).to eq("redis://localhost:6379")
232
+ end
233
+
234
+ it "should allow for redis url to be configured" do
235
+ @config.redis = "custom_redis_url"
236
+ expect(@config.redis).to eq("custom_redis_url")
237
+ end
238
+
239
+ context "provided REDIS_URL environment variable" do
240
+ it "should use the ENV variable" do
241
+ ENV['REDIS_URL'] = "env_redis_url"
242
+ expect(Split::Configuration.new.redis).to eq("env_redis_url")
243
+ end
244
+ end
245
+ end
246
+
247
+ context "persistence cookie length" do
248
+ it "should default to 1 year" do
249
+ expect(@config.persistence_cookie_length).to eq(31536000)
250
+ end
251
+
252
+ it "should allow the persistence cookie length to be configured" do
253
+ @config.persistence_cookie_length = 2592000
254
+ expect(@config.persistence_cookie_length).to eq(2592000)
255
+ end
256
+ end
257
+
258
+ end
@@ -0,0 +1,200 @@
1
+ require 'spec_helper'
2
+ require 'split/dashboard/pagination_helpers'
3
+
4
+ describe Split::DashboardPaginationHelpers do
5
+ include Split::DashboardPaginationHelpers
6
+
7
+ let(:url) { '/split/' }
8
+
9
+ describe '#pagination_per' do
10
+ context 'when params empty' do
11
+ let(:params) { Hash[] }
12
+
13
+ it 'returns the default (10)' do
14
+ default_per_page = Split.configuration.dashboard_pagination_default_per_page
15
+ expect(pagination_per).to eql default_per_page
16
+ expect(pagination_per).to eql 10
17
+ end
18
+ end
19
+
20
+ context 'when params[:per] is 5' do
21
+ let(:params) { Hash[per: 5] }
22
+
23
+ it 'returns 5' do
24
+ expect(pagination_per).to eql 5
25
+ end
26
+ end
27
+ end
28
+
29
+ describe '#page_number' do
30
+ context 'when params empty' do
31
+ let(:params) { Hash[] }
32
+
33
+ it 'returns 1' do
34
+ expect(page_number).to eql 1
35
+ end
36
+ end
37
+
38
+ context 'when params[:page] is "2"' do
39
+ let(:params) { Hash[page: '2'] }
40
+
41
+ it 'returns 2' do
42
+ expect(page_number).to eql 2
43
+ end
44
+ end
45
+ end
46
+
47
+ describe '#paginated' do
48
+ let(:collection) { (1..20).to_a }
49
+ let(:params) { Hash[per: '5', page: '3'] }
50
+
51
+ it { expect(paginated(collection)).to eql [11, 12, 13, 14, 15] }
52
+ end
53
+
54
+ describe '#show_first_page_tag?' do
55
+ context 'when page is 1' do
56
+ it { expect(show_first_page_tag?).to be false }
57
+ end
58
+
59
+ context 'when page is 3' do
60
+ let(:params) { Hash[page: '3'] }
61
+ it { expect(show_first_page_tag?).to be true }
62
+ end
63
+ end
64
+
65
+ describe '#first_page_tag' do
66
+ it { expect(first_page_tag).to eql '<a href="/split?page=1&per=10">1</a>' }
67
+ end
68
+
69
+ describe '#show_first_ellipsis_tag?' do
70
+ context 'when page is 1' do
71
+ it { expect(show_first_ellipsis_tag?).to be false }
72
+ end
73
+
74
+ context 'when page is 4' do
75
+ let(:params) { Hash[page: '4'] }
76
+ it { expect(show_first_ellipsis_tag?).to be true }
77
+ end
78
+ end
79
+
80
+ describe '#ellipsis_tag' do
81
+ it { expect(ellipsis_tag).to eql '<span>...</span>' }
82
+ end
83
+
84
+ describe '#show_prev_page_tag?' do
85
+ context 'when page is 1' do
86
+ it { expect(show_prev_page_tag?).to be false }
87
+ end
88
+
89
+ context 'when page is 2' do
90
+ let(:params) { Hash[page: '2'] }
91
+ it { expect(show_prev_page_tag?).to be true }
92
+ end
93
+ end
94
+
95
+ describe '#prev_page_tag' do
96
+ context 'when page is 2' do
97
+ let(:params) { Hash[page: '2'] }
98
+
99
+ it do
100
+ expect(prev_page_tag).to eql '<a href="/split?page=1&per=10">1</a>'
101
+ end
102
+ end
103
+
104
+ context 'when page is 3' do
105
+ let(:params) { Hash[page: '3'] }
106
+
107
+ it do
108
+ expect(prev_page_tag).to eql '<a href="/split?page=2&per=10">2</a>'
109
+ end
110
+ end
111
+ end
112
+
113
+ describe '#show_prev_page_tag?' do
114
+ context 'when page is 1' do
115
+ it { expect(show_prev_page_tag?).to be false }
116
+ end
117
+
118
+ context 'when page is 2' do
119
+ let(:params) { Hash[page: '2'] }
120
+ it { expect(show_prev_page_tag?).to be true }
121
+ end
122
+ end
123
+
124
+ describe '#current_page_tag' do
125
+ context 'when page is 1' do
126
+ let(:params) { Hash[page: '1'] }
127
+ it { expect(current_page_tag).to eql '<span><b>1</b></span>' }
128
+ end
129
+
130
+ context 'when page is 2' do
131
+ let(:params) { Hash[page: '2'] }
132
+ it { expect(current_page_tag).to eql '<span><b>2</b></span>' }
133
+ end
134
+ end
135
+
136
+ describe '#show_next_page_tag?' do
137
+ context 'when page is 2' do
138
+ let(:params) { Hash[page: '2'] }
139
+
140
+ context 'when collection length is 20' do
141
+ let(:collection) { (1..20).to_a }
142
+ it { expect(show_next_page_tag?(collection)).to be false }
143
+ end
144
+
145
+ context 'when collection length is 25' do
146
+ let(:collection) { (1..25).to_a }
147
+ it { expect(show_next_page_tag?(collection)).to be true }
148
+ end
149
+ end
150
+ end
151
+
152
+ describe '#next_page_tag' do
153
+ context 'when page is 1' do
154
+ let(:params) { Hash[page: '1'] }
155
+ it { expect(next_page_tag).to eql '<a href="/split?page=2&per=10">2</a>' }
156
+ end
157
+
158
+ context 'when page is 2' do
159
+ let(:params) { Hash[page: '2'] }
160
+ it { expect(next_page_tag).to eql '<a href="/split?page=3&per=10">3</a>' }
161
+ end
162
+ end
163
+
164
+ describe '#total_pages' do
165
+ context 'when collection length is 30' do
166
+ let(:collection) { (1..30).to_a }
167
+ it { expect(total_pages(collection)).to eql 3 }
168
+ end
169
+
170
+ context 'when collection length is 35' do
171
+ let(:collection) { (1..35).to_a }
172
+ it { expect(total_pages(collection)).to eql 4 }
173
+ end
174
+ end
175
+
176
+ describe '#show_last_ellipsis_tag?' do
177
+ let(:collection) { (1..30).to_a }
178
+ let(:params) { Hash[per: '5', page: '2'] }
179
+ it { expect(show_last_ellipsis_tag?(collection)).to be true }
180
+ end
181
+
182
+ describe '#show_last_page_tag?' do
183
+ let(:collection) { (1..30).to_a }
184
+
185
+ context 'when page is 5/6' do
186
+ let(:params) { Hash[per: '5', page: '5'] }
187
+ it { expect(show_last_page_tag?(collection)).to be false }
188
+ end
189
+
190
+ context 'when page is 4/6' do
191
+ let(:params) { Hash[per: '5', page: '4'] }
192
+ it { expect(show_last_page_tag?(collection)).to be true }
193
+ end
194
+ end
195
+
196
+ describe '#last_page_tag' do
197
+ let(:collection) { (1..30).to_a }
198
+ it { expect(last_page_tag(collection)).to eql '<a href="/split?page=3&per=10">3</a>' }
199
+ end
200
+ end