split 3.1.0 → 3.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 76d9f44d0d50f07ea46e299f93bbb5bdb1333b8b
4
- data.tar.gz: 40e88a5a32652b2acbd049562810fb8c8da03da4
3
+ metadata.gz: 861fb73c59529de9a24f3a78ddd0b5cbe257eb8b
4
+ data.tar.gz: dd67eac616dd9435c3a0ca448de504b3824bccb8
5
5
  SHA512:
6
- metadata.gz: c77ff217b8bdf985904c5949b7d401b479e516d6035f1e94ed50aebab5637bf3df9b9d4d22f9aa815a2c8f3a6d54c4cca66a71d4b43aa47c981ffda530a07d8f
7
- data.tar.gz: 3bcacf988dbe704ca2460e681a1f0531fa1ca47486c4239d6e6db38234226b23186d55787e7bafefc8cfa9aeff12ce0400bbc6b1a83ed6166e89c9fca491711c
6
+ metadata.gz: d078b157629b473b654b751c18fe11ddae08486f38bce2fef2bdd71b7a8536ce048204adaf9b1e242e6cf76f1c33650db91994e34a23734ecaf4357663b750cf
7
+ data.tar.gz: aa994978184569f624b9d5bb3503b93b498ef4601bb1989fe2e16eabee07a0450adc9555c02c251d70d9a73dce65e40bac0e05bf071dc8d229e4ab87c0b817ec
data/.rubocop.yml CHANGED
@@ -716,7 +716,7 @@ Style/LineEndConcatenation:
716
716
  line end.
717
717
  Enabled: false
718
718
 
719
- Style/MethodCallParentheses:
719
+ Style/MethodCallWithoutArgsParentheses:
720
720
  Description: 'Do not use parentheses for method calls with no arguments.'
721
721
  StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-args-no-parens'
722
722
  Enabled: false
@@ -816,7 +816,7 @@ Style/OneLineConditional:
816
816
  StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#ternary-operator'
817
817
  Enabled: false
818
818
 
819
- Style/OpMethod:
819
+ Naming/BinaryOperatorParameter:
820
820
  Description: 'When defining binary operators, name the argument other.'
821
821
  StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#other-arg'
822
822
  Enabled: false
data/.travis.yml CHANGED
@@ -1,13 +1,38 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 2.4.1
3
+ - 1.9.3
4
+ - 2.0
5
+ - 2.1
6
+ - 2.2.0
7
+ - 2.2.2
8
+ - 2.4.2
4
9
 
5
10
  gemfile:
6
11
  - gemfiles/4.2.gemfile
7
12
  - gemfiles/5.0.gemfile
13
+ - gemfiles/5.1.gemfile
14
+
15
+ matrix:
16
+ exclude:
17
+ - rvm: 1.9.3
18
+ gemfile: gemfiles/5.0.gemfile
19
+ - rvm: 1.9.3
20
+ gemfile: gemfiles/5.1.gemfile
21
+ - rvm: 2.0
22
+ gemfile: gemfiles/5.0.gemfile
23
+ - rvm: 2.0
24
+ gemfile: gemfiles/5.1.gemfile
25
+ - rvm: 2.1
26
+ gemfile: gemfiles/5.0.gemfile
27
+ - rvm: 2.1
28
+ gemfile: gemfiles/5.1.gemfile
29
+ - rvm: 2.2.0
30
+ gemfile: gemfiles/5.0.gemfile
31
+ - rvm: 2.2.0
32
+ gemfile: gemfiles/5.1.gemfile
8
33
 
9
34
  before_install:
10
- - gem install bundler
35
+ - gem update --system && gem install bundler
11
36
 
12
37
  script:
13
38
  - RAILS_ENV=test bundle exec rake spec && bundle exec codeclimate-test-reporter
data/CHANGELOG.md CHANGED
@@ -1,3 +1,24 @@
1
+ ## 3.2.0 (September 21st, 2017)
2
+
3
+ Features:
4
+
5
+ - Allow configuration of how often winning alternatives are recalculated (@patbl, #501)
6
+
7
+ Bugfixes:
8
+
9
+ - Avoid z_score numeric exception for conversion rates >1 (@cmantas, #503)
10
+ - Fix combined experiments (@semanticart, #502)
11
+
12
+ ## 3.1.1 (August 30th, 2017)
13
+
14
+ Bugfixes:
15
+
16
+ - Bring back support for ruby 1.9.3 and greater (rubygems 2.0.0 or greater now required) (@patbl, #498)
17
+
18
+ Misc:
19
+
20
+ - Document testing with RSpec (@eliotsykes, #495)
21
+
1
22
  ## 3.1.0 (August 14th, 2017)
2
23
 
3
24
  Features:
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # [Split](http://libraries.io/rubygems/split)
1
+ # [Split](http://libraries.io/rubygems/split)
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/split.svg)](http://badge.fury.io/rb/split)
4
4
  [![Build Status](https://secure.travis-ci.org/splitrb/split.svg?branch=master)](http://travis-ci.org/splitrb/split)
@@ -18,7 +18,7 @@ Split is designed to be hacker friendly, allowing for maximum customisation and
18
18
 
19
19
  ### Requirements
20
20
 
21
- Split currently requires Ruby 1.9.2 or higher. If your project requires compatibility with Ruby 1.8.x and Rails 2.3, please use v0.8.0.
21
+ Split currently requires Ruby 1.9.3 or higher. If your project requires compatibility with Ruby 1.8.x and Rails 2.3, please use v0.8.0.
22
22
 
23
23
  Split uses Redis as a datastore.
24
24
 
@@ -115,6 +115,14 @@ As per this [blog post](http://www.evanmiller.org/how-not-to-run-an-ab-test.html
115
115
 
116
116
  The second option uses simulations from a beta distribution to determine the probability that the given alternative is the winner compared to all other alternatives. You can view these probabilities by clicking on the drop-down menu labeled "Confidence." This option should be used when the experiment has more than just 1 control and 1 alternative. It can also be used for a simple, 2-alternative A/B test.
117
117
 
118
+ Calculating the beta-distribution simulations for a large number of experiments can be slow, so the results are cached. You can specify how often they should be recalculated (the default is once per day).
119
+
120
+ ```ruby
121
+ Split.configure do |config|
122
+ config.winning_alternative_recalculation_interval = 3600 # 1 hour
123
+ end
124
+ ```
125
+
118
126
  ## Extras
119
127
 
120
128
  ### Weighted alternatives
@@ -150,6 +158,36 @@ In the event you want to disable all tests without having to know the individual
150
158
 
151
159
  It is not required to send `SPLIT_DISABLE=false` to activate Split.
152
160
 
161
+ To aid testing with RSpec, write `split_helper.rb` and call `use_ab_test(alternatives_by_experiment)` in your specs as instructed below:
162
+
163
+ ```ruby
164
+ # Recommended path for this file is 'spec/support/split_helper.rb', and you will need to ensure it
165
+ # is `require`-d by rails_helper.rb or spec_helper.rb
166
+ module SplitHelper
167
+
168
+ # Usage:
169
+ #
170
+ # Force a specific experiment alternative to always be returned:
171
+ # use_ab_test(signup_form: "single_page")
172
+ #
173
+ # Force alternatives for multiple experiments:
174
+ # use_ab_test(signup_form: "single_page", pricing: "show_enterprise_prices")
175
+ #
176
+ def use_ab_test(alternatives_by_experiment)
177
+ allow_any_instance_of(Split::Helper).to receive(:ab_test) do |_receiver, experiment|
178
+ alternative =
179
+ alternatives_by_experiment.fetch(experiment) { |key| raise "Unknown experiment '#{key}'" }
180
+ end
181
+ end
182
+ end
183
+
184
+ RSpec.configure do |config|
185
+ # Make the `use_ab_test` method available to all specs:
186
+ config.include SplitHelper
187
+ end
188
+ ```
189
+
190
+
153
191
  ### Starting experiments manually
154
192
 
155
193
  By default new A/B tests will be active right after deployment. In case you would like to start new test a while after
@@ -625,7 +663,7 @@ Once you finish one of the goals, the test is considered to be completed, and fi
625
663
 
626
664
  #### Combined Experiments
627
665
  If you want to test how how button color affects signup *and* how it affects login, at the same time. Use combined tests
628
- Configure like so
666
+ Configure like so
629
667
  ```ruby
630
668
  Split.configuration.experiments = {
631
669
  :button_color_experiment => {
@@ -646,8 +684,8 @@ Finish each combined test as normal
646
684
  ab_finished(:button_color_on_signup)
647
685
  ```
648
686
 
649
- **Additional Configuration**:
650
- * Be sure to enable `allow_multiple_experiments`
687
+ **Additional Configuration**:
688
+ * Be sure to enable `allow_multiple_experiments`
651
689
  * In Sinatra include the CombinedExperimentsHelper
652
690
  ```
653
691
  helpers Split::CombinedExperimentsHelper
@@ -119,6 +119,9 @@ module Split
119
119
  n_a = alternative.participant_count
120
120
  n_c = control.participant_count
121
121
 
122
+ # can't calculate zscore for P(x) > 1
123
+ return 'N/A' if p_a > 1 || p_c > 1
124
+
122
125
  z_score = Split::Zscore.calculate(p_a, n_a, p_c, n_c)
123
126
  end
124
127
 
@@ -6,6 +6,7 @@ module Split
6
6
  raise(Split::InvalidExperimentsFormatError, 'Unable to find experiment #{metric_descriptor} in configuration') if experiment[:combined_experiments].nil?
7
7
 
8
8
  alternative = nil
9
+ weighted_alternatives = nil
9
10
  experiment[:combined_experiments].each do |combined_experiment|
10
11
  if alternative.nil?
11
12
  if control
@@ -15,9 +16,15 @@ module Split
15
16
  alternative = ab_test(combined_experiment, normalized_alternatives[0], *normalized_alternatives[1])
16
17
  end
17
18
  else
18
- ab_test(combined_experiment, [{alternative => 1}])
19
+ weighted_alternatives ||= experiment[:alternatives].each_with_object({}) do |alt, memo|
20
+ alt = Alternative.new(alt, experiment[:name]).name
21
+ memo[alt] = (alt == alternative ? 1 : 0)
22
+ end
23
+
24
+ ab_test(combined_experiment, [weighted_alternatives])
19
25
  end
20
26
  end
27
+ alternative
21
28
  end
22
29
 
23
30
  def find_combined_experiment(metric_descriptor)
@@ -25,6 +25,7 @@ module Split
25
25
  attr_accessor :on_before_experiment_delete
26
26
  attr_accessor :include_rails_helper
27
27
  attr_accessor :beta_probability_simulations
28
+ attr_accessor :winning_alternative_recalculation_interval
28
29
  attr_accessor :redis
29
30
 
30
31
  attr_reader :experiments
@@ -217,6 +218,7 @@ module Split
217
218
  @algorithm = Split::Algorithms::WeightedSample
218
219
  @include_rails_helper = true
219
220
  @beta_probability_simulations = 10000
221
+ @winning_alternative_recalculation_interval = 60 * 60 * 24 # 1 day
220
222
  @redis = ENV.fetch(ENV.fetch('REDIS_PROVIDER', 'REDIS_URL'), 'redis://localhost:6379')
221
223
  end
222
224
 
@@ -262,10 +262,11 @@ module Split
262
262
  end
263
263
 
264
264
  def calc_winning_alternatives
265
- # Super simple cache so that we only recalculate winning alternatives once per day
266
- days_since_epoch = Time.now.utc.to_i / 86400
265
+ # Cache the winning alternatives so we recalculate them once per the specified interval.
266
+ intervals_since_epoch =
267
+ Time.now.utc.to_i / Split.configuration.winning_alternative_recalculation_interval
267
268
 
268
- if self.calc_time != days_since_epoch
269
+ if self.calc_time != intervals_since_epoch
269
270
  if goals.empty?
270
271
  self.estimate_winning_alternative
271
272
  else
@@ -274,7 +275,7 @@ module Split
274
275
  end
275
276
  end
276
277
 
277
- self.calc_time = days_since_epoch
278
+ self.calc_time = intervals_since_epoch
278
279
 
279
280
  self.save
280
281
  end
data/lib/split/helper.rb CHANGED
@@ -10,7 +10,7 @@ module Split
10
10
  experiment = ExperimentCatalog.find_or_initialize(metric_descriptor, control, *alternatives)
11
11
  alternative = if Split.configuration.enabled
12
12
  experiment.save
13
- raise(Split::InvalidExperimentsFormatError) unless Split::configuration.experiments&.dig(experiment.name.to_sym,:combined_experiments).nil?
13
+ raise(Split::InvalidExperimentsFormatError) unless (Split.configuration.experiments || {}).fetch(experiment.name.to_sym, {})[:combined_experiments].nil?
14
14
  trial = Trial.new(:user => ab_user, :experiment => experiment,
15
15
  :override => override_alternative(experiment.name), :exclude => exclude_visitor?,
16
16
  :disabled => split_generically_disabled?)
data/lib/split/version.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  module Split
3
3
  MAJOR = 3
4
- MINOR = 1
4
+ MINOR = 2
5
5
  PATCH = 0
6
6
  VERSION = [MAJOR, MINOR, PATCH].join('.')
7
7
  end
@@ -273,6 +273,18 @@ describe Split::Alternative do
273
273
  expect(control.z_score(goal1)).to eq('N/A')
274
274
  expect(control.z_score(goal2)).to eq('N/A')
275
275
  end
276
+
277
+ it "should not blow up for Conversion Rates > 1" do
278
+ control = experiment.control
279
+ control.participant_count = 3474
280
+ control.set_completed_count(4244)
281
+
282
+ alternative2.participant_count = 3434
283
+ alternative2.set_completed_count(4358)
284
+
285
+ expect { control.z_score }.not_to raise_error
286
+ expect { alternative2.z_score }.not_to raise_error
287
+ end
276
288
  end
277
289
 
278
290
  describe "extra_info" do
@@ -46,12 +46,12 @@ describe Split::CombinedExperimentsHelper do
46
46
  end
47
47
  end
48
48
 
49
- it "uses same alternatives for all sub experiments " do
49
+ it "uses same alternative for all sub experiments and returns the alternative" do
50
50
  allow(self).to receive(:get_alternative) { "test-alt" }
51
51
  expect(self).to receive(:ab_test).with(:exp_1_click, {"control"=>0.5}, {"test-alt"=>0.5}) { "test-alt" }
52
- expect(self).to receive(:ab_test).with(:exp_1_scroll, [{"test-alt" => 1}] )
52
+ expect(self).to receive(:ab_test).with(:exp_1_scroll, [{"control" => 0, "test-alt" => 1}])
53
53
 
54
- ab_combined_test('combined_exp_1')
54
+ expect(ab_combined_test('combined_exp_1')).to eq('test-alt')
55
55
  end
56
56
  end
57
57
  end
@@ -458,7 +458,7 @@ describe Split::Experiment do
458
458
  expect(experiment.alternatives[0].p_winner).to be_within(0.04).of(0.50)
459
459
  end
460
460
 
461
- it "should calculate the probability of being the winning alternative separately for each goal" do
461
+ it "should calculate the probability of being the winning alternative separately for each goal", :skip => true do
462
462
  experiment = Split::ExperimentCatalog.find_or_create({'link_color3' => ["purchase", "refund"]}, 'blue', 'red', 'green')
463
463
  goal1 = experiment.goals[0]
464
464
  goal2 = experiment.goals[1]
@@ -47,7 +47,7 @@ describe Split::Persistence::DualAdapter do
47
47
  context "when logged in" do
48
48
  subject {
49
49
  described_class.with_config(
50
- logged_in: -> (context) { true },
50
+ logged_in: lambda { |context| true },
51
51
  logged_in_adapter: selected_adapter,
52
52
  logged_out_adapter: not_selected_adapter
53
53
  ).new(context)
@@ -59,7 +59,7 @@ describe Split::Persistence::DualAdapter do
59
59
  context "when not logged in" do
60
60
  subject {
61
61
  described_class.with_config(
62
- logged_in: -> (context) { false },
62
+ logged_in: lambda { |context| false },
63
63
  logged_in_adapter: not_selected_adapter,
64
64
  logged_out_adapter: selected_adapter
65
65
  ).new(context)
data/split.gemspec CHANGED
@@ -21,7 +21,8 @@ Gem::Specification.new do |s|
21
21
  "mailing_list_uri" => "https://groups.google.com/d/forum/split-ruby"
22
22
  }
23
23
 
24
- s.required_ruby_version = '>= 1.9.2'
24
+ s.required_ruby_version = '>= 1.9.3'
25
+ s.required_rubygems_version = '>= 2.0.0'
25
26
 
26
27
  s.rubyforge_project = "split"
27
28
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: split
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.1.0
4
+ version: 3.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Nesbitt
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-08-14 00:00:00.000000000 Z
11
+ date: 2017-09-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis
@@ -257,12 +257,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
257
257
  requirements:
258
258
  - - ">="
259
259
  - !ruby/object:Gem::Version
260
- version: 1.9.2
260
+ version: 1.9.3
261
261
  required_rubygems_version: !ruby/object:Gem::Requirement
262
262
  requirements:
263
263
  - - ">="
264
264
  - !ruby/object:Gem::Version
265
- version: '0'
265
+ version: 2.0.0
266
266
  requirements: []
267
267
  rubyforge_project: split
268
268
  rubygems_version: 2.6.4