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,31 @@
1
+ # frozen_string_literal: true
2
+ require 'spec_helper'
3
+ require 'split/metric'
4
+
5
+ describe Split::Metric do
6
+ describe 'possible experiments' do
7
+ it "should load the experiment if there is one, but no metric" do
8
+ experiment = Split::ExperimentCatalog.find_or_create('color', 'red', 'blue')
9
+ expect(Split::Metric.possible_experiments('color')).to eq([experiment])
10
+ end
11
+
12
+ it "should load the experiments in a metric" do
13
+ experiment1 = Split::ExperimentCatalog.find_or_create('color', 'red', 'blue')
14
+ experiment2 = Split::ExperimentCatalog.find_or_create('size', 'big', 'small')
15
+
16
+ metric = Split::Metric.new(:name => 'purchase', :experiments => [experiment1, experiment2])
17
+ metric.save
18
+ expect(Split::Metric.possible_experiments('purchase')).to include(experiment1, experiment2)
19
+ end
20
+
21
+ it "should load both the metric experiments and an experiment with the same name" do
22
+ experiment1 = Split::ExperimentCatalog.find_or_create('purchase', 'red', 'blue')
23
+ experiment2 = Split::ExperimentCatalog.find_or_create('size', 'big', 'small')
24
+
25
+ metric = Split::Metric.new(:name => 'purchase', :experiments => [experiment2])
26
+ metric.save
27
+ expect(Split::Metric.possible_experiments('purchase')).to include(experiment1, experiment2)
28
+ end
29
+ end
30
+
31
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+ require "spec_helper"
3
+ require 'rack/test'
4
+
5
+ describe Split::Persistence::CookieAdapter do
6
+ subject { described_class.new(context) }
7
+
8
+ shared_examples "sets cookies correctly" do
9
+ describe "#[] and #[]=" do
10
+ it "set and return the value for given key" do
11
+ subject["my_key"] = "my_value"
12
+ expect(subject["my_key"]).to eq("my_value")
13
+ end
14
+
15
+ it "handles invalid JSON" do
16
+ context.request.cookies[:split] = {
17
+ :value => '{"foo":2,',
18
+ :expires => Time.now
19
+ }
20
+ expect(subject["my_key"]).to be_nil
21
+ subject["my_key"] = "my_value"
22
+ expect(subject["my_key"]).to eq("my_value")
23
+ end
24
+ end
25
+
26
+ describe "#delete" do
27
+ it "should delete the given key" do
28
+ subject["my_key"] = "my_value"
29
+ subject.delete("my_key")
30
+ expect(subject["my_key"]).to be_nil
31
+ end
32
+ end
33
+
34
+ describe "#keys" do
35
+ it "should return an array of the session's stored keys" do
36
+ subject["my_key"] = "my_value"
37
+ subject["my_second_key"] = "my_second_value"
38
+ expect(subject.keys).to match(["my_key", "my_second_key"])
39
+ end
40
+ end
41
+ end
42
+
43
+
44
+ context "when using Rack" do
45
+ let(:env) { Rack::MockRequest.env_for("http://example.com:8080/") }
46
+ let(:request) { Rack::Request.new(env) }
47
+ let(:response) { Rack::MockResponse.new(200, {}, "") }
48
+ let(:context) { double(request: request, response: response, cookies: CookiesMock.new) }
49
+
50
+ include_examples "sets cookies correctly"
51
+
52
+ it "puts multiple experiments in a single cookie" do
53
+ subject["foo"] = "FOO"
54
+ subject["bar"] = "BAR"
55
+ expect(context.response.headers["Set-Cookie"]).to match(/\Asplit=%7B%22foo%22%3A%22FOO%22%2C%22bar%22%3A%22BAR%22%7D; path=\/; expires=[a-zA-Z]{3}, \d{2} [a-zA-Z]{3} \d{4} \d{2}:\d{2}:\d{2} -0000\Z/)
56
+ end
57
+
58
+ it "ensure other added cookies are not overriden" do
59
+ context.response.set_cookie 'dummy', 'wow'
60
+ subject["foo"] = "FOO"
61
+ expect(context.response.headers["Set-Cookie"]).to include("dummy=wow")
62
+ expect(context.response.headers["Set-Cookie"]).to include("split=")
63
+ end
64
+ end
65
+
66
+ context "when @context is an ActionController::Base" do
67
+ before :context do
68
+ require "rails"
69
+ require "action_controller/railtie"
70
+ end
71
+
72
+ let(:context) do
73
+ controller = controller_class.new
74
+ if controller.respond_to?(:set_request!)
75
+ controller.set_request!(ActionDispatch::Request.new({}))
76
+ else # Before rails 5.0
77
+ controller.send(:"request=", ActionDispatch::Request.new({}))
78
+ end
79
+
80
+ response = ActionDispatch::Response.new(200, {}, '').tap do |res|
81
+ res.request = controller.request
82
+ end
83
+
84
+ if controller.respond_to?(:set_response!)
85
+ controller.set_response!(response)
86
+ else # Before rails 5.0
87
+ controller.send(:set_response!, response)
88
+ end
89
+ controller
90
+ end
91
+
92
+ let(:controller_class) { Class.new(ActionController::Base) }
93
+
94
+ include_examples "sets cookies correctly"
95
+
96
+ it "puts multiple experiments in a single cookie" do
97
+ subject["foo"] = "FOO"
98
+ subject["bar"] = "BAR"
99
+ expect(subject.keys).to eq(["foo", "bar"])
100
+ expect(subject["foo"]).to eq("FOO")
101
+ expect(subject["bar"]).to eq("BAR")
102
+ cookie_jar = context.request.env["action_dispatch.cookies"]
103
+ expect(cookie_jar['split']).to eq("{\"foo\":\"FOO\",\"bar\":\"BAR\"}")
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,194 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Split::Persistence::DualAdapter do
6
+ let(:context) { 'some context' }
7
+
8
+ let(:logged_in_adapter_instance) { double }
9
+ let(:logged_in_adapter) do
10
+ Class.new.tap { |c| allow(c).to receive(:new) { logged_in_adapter_instance } }
11
+ end
12
+ let(:logged_out_adapter_instance) { double }
13
+ let(:logged_out_adapter) do
14
+ Class.new.tap { |c| allow(c).to receive(:new) { logged_out_adapter_instance } }
15
+ end
16
+
17
+ context 'when fallback_to_logged_out_adapter is false' do
18
+ context 'when logged in' do
19
+ subject do
20
+ described_class.with_config(
21
+ logged_in: lambda { |context| true },
22
+ logged_in_adapter: logged_in_adapter,
23
+ logged_out_adapter: logged_out_adapter,
24
+ fallback_to_logged_out_adapter: false
25
+ ).new(context)
26
+ end
27
+
28
+ it '#[]=' do
29
+ expect(logged_in_adapter_instance).to receive(:[]=).with('my_key', 'my_value')
30
+ expect_any_instance_of(logged_out_adapter).not_to receive(:[]=)
31
+ subject['my_key'] = 'my_value'
32
+ end
33
+
34
+ it '#[]' do
35
+ expect(logged_in_adapter_instance).to receive(:[]).with('my_key') { 'my_value' }
36
+ expect_any_instance_of(logged_out_adapter).not_to receive(:[])
37
+ expect(subject['my_key']).to eq('my_value')
38
+ end
39
+
40
+ it '#delete' do
41
+ expect(logged_in_adapter_instance).to receive(:delete).with('my_key') { 'my_value' }
42
+ expect_any_instance_of(logged_out_adapter).not_to receive(:delete)
43
+ expect(subject.delete('my_key')).to eq('my_value')
44
+ end
45
+
46
+ it '#keys' do
47
+ expect(logged_in_adapter_instance).to receive(:keys) { ['my_value'] }
48
+ expect_any_instance_of(logged_out_adapter).not_to receive(:keys)
49
+ expect(subject.keys).to eq(['my_value'])
50
+ end
51
+ end
52
+
53
+ context 'when logged out' do
54
+ subject do
55
+ described_class.with_config(
56
+ logged_in: lambda { |context| false },
57
+ logged_in_adapter: logged_in_adapter,
58
+ logged_out_adapter: logged_out_adapter,
59
+ fallback_to_logged_out_adapter: false
60
+ ).new(context)
61
+ end
62
+
63
+ it '#[]=' do
64
+ expect_any_instance_of(logged_in_adapter).not_to receive(:[]=)
65
+ expect(logged_out_adapter_instance).to receive(:[]=).with('my_key', 'my_value')
66
+ subject['my_key'] = 'my_value'
67
+ end
68
+
69
+ it '#[]' do
70
+ expect_any_instance_of(logged_in_adapter).not_to receive(:[])
71
+ expect(logged_out_adapter_instance).to receive(:[]).with('my_key') { 'my_value' }
72
+ expect(subject['my_key']).to eq('my_value')
73
+ end
74
+
75
+ it '#delete' do
76
+ expect_any_instance_of(logged_in_adapter).not_to receive(:delete)
77
+ expect(logged_out_adapter_instance).to receive(:delete).with('my_key') { 'my_value' }
78
+ expect(subject.delete('my_key')).to eq('my_value')
79
+ end
80
+
81
+ it '#keys' do
82
+ expect_any_instance_of(logged_in_adapter).not_to receive(:keys)
83
+ expect(logged_out_adapter_instance).to receive(:keys) { ['my_value', 'my_value2'] }
84
+ expect(subject.keys).to eq(['my_value', 'my_value2'])
85
+ end
86
+ end
87
+ end
88
+
89
+ context 'when fallback_to_logged_out_adapter is true' do
90
+ context 'when logged in' do
91
+ subject do
92
+ described_class.with_config(
93
+ logged_in: lambda { |context| true },
94
+ logged_in_adapter: logged_in_adapter,
95
+ logged_out_adapter: logged_out_adapter,
96
+ fallback_to_logged_out_adapter: true
97
+ ).new(context)
98
+ end
99
+
100
+ it '#[]=' do
101
+ expect(logged_in_adapter_instance).to receive(:[]=).with('my_key', 'my_value')
102
+ expect(logged_out_adapter_instance).to receive(:[]=).with('my_key', 'my_value')
103
+ expect(logged_out_adapter_instance).to receive(:[]).with('my_key') { nil }
104
+ subject['my_key'] = 'my_value'
105
+ end
106
+
107
+ it '#[]' do
108
+ expect(logged_in_adapter_instance).to receive(:[]).with('my_key') { 'my_value' }
109
+ expect_any_instance_of(logged_out_adapter).not_to receive(:[])
110
+ expect(subject['my_key']).to eq('my_value')
111
+ end
112
+
113
+ it '#delete' do
114
+ expect(logged_in_adapter_instance).to receive(:delete).with('my_key') { 'my_value' }
115
+ expect(logged_out_adapter_instance).to receive(:delete).with('my_key') { 'my_value' }
116
+ expect(subject.delete('my_key')).to eq('my_value')
117
+ end
118
+
119
+ it '#keys' do
120
+ expect(logged_in_adapter_instance).to receive(:keys) { ['my_value'] }
121
+ expect(logged_out_adapter_instance).to receive(:keys) { ['my_value', 'my_value2'] }
122
+ expect(subject.keys).to eq(['my_value', 'my_value2'])
123
+ end
124
+ end
125
+
126
+ context 'when logged out' do
127
+ subject do
128
+ described_class.with_config(
129
+ logged_in: lambda { |context| false },
130
+ logged_in_adapter: logged_in_adapter,
131
+ logged_out_adapter: logged_out_adapter,
132
+ fallback_to_logged_out_adapter: true
133
+ ).new(context)
134
+ end
135
+
136
+ it '#[]=' do
137
+ expect_any_instance_of(logged_in_adapter).not_to receive(:[]=)
138
+ expect(logged_out_adapter_instance).to receive(:[]=).with('my_key', 'my_value')
139
+ expect(logged_out_adapter_instance).to receive(:[]).with('my_key') { nil }
140
+ subject['my_key'] = 'my_value'
141
+ end
142
+
143
+ it '#[]' do
144
+ expect_any_instance_of(logged_in_adapter).not_to receive(:[])
145
+ expect(logged_out_adapter_instance).to receive(:[]).with('my_key') { 'my_value' }
146
+ expect(subject['my_key']).to eq('my_value')
147
+ end
148
+
149
+ it '#delete' do
150
+ expect(logged_in_adapter_instance).to receive(:delete).with('my_key') { 'my_value' }
151
+ expect(logged_out_adapter_instance).to receive(:delete).with('my_key') { 'my_value' }
152
+ expect(subject.delete('my_key')).to eq('my_value')
153
+ end
154
+
155
+ it '#keys' do
156
+ expect(logged_in_adapter_instance).to receive(:keys) { ['my_value'] }
157
+ expect(logged_out_adapter_instance).to receive(:keys) { ['my_value', 'my_value2'] }
158
+ expect(subject.keys).to eq(['my_value', 'my_value2'])
159
+ end
160
+ end
161
+ end
162
+
163
+ describe 'when errors in config' do
164
+ before { described_class.config.clear }
165
+ let(:some_proc) { ->{} }
166
+
167
+ it 'when no logged in adapter' do
168
+ expect{
169
+ described_class.with_config(
170
+ logged_in: some_proc,
171
+ logged_out_adapter: logged_out_adapter
172
+ ).new(context)
173
+ }.to raise_error(StandardError, /:logged_in_adapter/)
174
+ end
175
+
176
+ it 'when no logged out adapter' do
177
+ expect{
178
+ described_class.with_config(
179
+ logged_in: some_proc,
180
+ logged_in_adapter: logged_in_adapter
181
+ ).new(context)
182
+ }.to raise_error(StandardError, /:logged_out_adapter/)
183
+ end
184
+
185
+ it 'when no logged in detector' do
186
+ expect{
187
+ described_class.with_config(
188
+ logged_in_adapter: logged_in_adapter,
189
+ logged_out_adapter: logged_out_adapter
190
+ ).new(context)
191
+ }.to raise_error(StandardError, /:logged_in$/)
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+ require "spec_helper"
3
+
4
+ describe Split::Persistence::RedisAdapter do
5
+
6
+ let(:context) { double(:lookup => 'blah') }
7
+
8
+ subject { Split::Persistence::RedisAdapter.new(context) }
9
+
10
+ describe '#redis_key' do
11
+ before { Split::Persistence::RedisAdapter.reset_config! }
12
+
13
+ context 'default' do
14
+ it 'should raise error with prompt to set lookup_by' do
15
+ expect{Split::Persistence::RedisAdapter.new(context)}.to raise_error(RuntimeError)
16
+ end
17
+ end
18
+
19
+ context 'config with key' do
20
+ before { Split::Persistence::RedisAdapter.reset_config! }
21
+ subject { Split::Persistence::RedisAdapter.new(context, 'manual') }
22
+
23
+ it 'should be "persistence:manual"' do
24
+ expect(subject.redis_key).to eq('persistence:manual')
25
+ end
26
+ end
27
+
28
+ context 'config with lookup_by = proc { "block" }' do
29
+ before { Split::Persistence::RedisAdapter.with_config(:lookup_by => proc{'block'}) }
30
+
31
+ it 'should be "persistence:block"' do
32
+ expect(subject.redis_key).to eq('persistence:block')
33
+ end
34
+ end
35
+
36
+ context 'config with lookup_by = proc { |context| context.test }' do
37
+ before { Split::Persistence::RedisAdapter.with_config(:lookup_by => proc{'block'}) }
38
+ let(:context) { double(:test => 'block') }
39
+
40
+ it 'should be "persistence:block"' do
41
+ expect(subject.redis_key).to eq('persistence:block')
42
+ end
43
+ end
44
+
45
+ context 'config with lookup_by = "method_name"' do
46
+ before { Split::Persistence::RedisAdapter.with_config(:lookup_by => 'method_name') }
47
+ let(:context) { double(:method_name => 'val') }
48
+
49
+ it 'should be "persistence:bar"' do
50
+ expect(subject.redis_key).to eq('persistence:val')
51
+ end
52
+ end
53
+
54
+ context 'config with namespace and lookup_by' do
55
+ before { Split::Persistence::RedisAdapter.with_config(:lookup_by => proc{'frag'}, :namespace => 'namer') }
56
+
57
+ it 'should be "namer"' do
58
+ expect(subject.redis_key).to eq('namer:frag')
59
+ end
60
+ end
61
+ end
62
+
63
+ context 'functional tests' do
64
+ before { Split::Persistence::RedisAdapter.with_config(:lookup_by => 'lookup') }
65
+
66
+ describe "#[] and #[]=" do
67
+ it "should set and return the value for given key" do
68
+ subject["my_key"] = "my_value"
69
+ expect(subject["my_key"]).to eq("my_value")
70
+ end
71
+ end
72
+
73
+ describe "#delete" do
74
+ it "should delete the given key" do
75
+ subject["my_key"] = "my_value"
76
+ subject.delete("my_key")
77
+ expect(subject["my_key"]).to be_nil
78
+ end
79
+ end
80
+
81
+ describe "#keys" do
82
+ it "should return an array of the user's stored keys" do
83
+ subject["my_key"] = "my_value"
84
+ subject["my_second_key"] = "my_second_value"
85
+ expect(subject.keys).to match(["my_key", "my_second_key"])
86
+ end
87
+ end
88
+
89
+ end
90
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+ require "spec_helper"
3
+
4
+ describe Split::Persistence::SessionAdapter do
5
+
6
+ let(:context) { double(:session => {}) }
7
+ subject { Split::Persistence::SessionAdapter.new(context) }
8
+
9
+ describe "#[] and #[]=" do
10
+ it "should set and return the value for given key" do
11
+ subject["my_key"] = "my_value"
12
+ expect(subject["my_key"]).to eq("my_value")
13
+ end
14
+ end
15
+
16
+ describe "#delete" do
17
+ it "should delete the given key" do
18
+ subject["my_key"] = "my_value"
19
+ subject.delete("my_key")
20
+ expect(subject["my_key"]).to be_nil
21
+ end
22
+ end
23
+
24
+ describe "#keys" do
25
+ it "should return an array of the session's stored keys" do
26
+ subject["my_key"] = "my_value"
27
+ subject["my_second_key"] = "my_second_value"
28
+ expect(subject.keys).to match(["my_key", "my_second_key"])
29
+ end
30
+ end
31
+
32
+ end