ab-split 1.0.0

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 (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