Skip to content

Commit ccff6dc

Browse files
daviddeningandrew
authored andcommitted
Support for combined experiments (see README) (#493)
* Support for combined experiments (see README) * Refactor combined experiments into its own helper * Update README, add descriptive error messages
1 parent e6ff873 commit ccff6dc

7 files changed

Lines changed: 133 additions & 1 deletion

File tree

README.md

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# [Split](http://libraries.io/rubygems/split)
1+
# [Split](http://libraries.io/rubygems/split)
22

33
[![Gem Version](https://badge.fury.io/rb/split.svg)](http://badge.fury.io/rb/split)
44
[![Build Status](https://secure.travis-ci.org/splitrb/split.svg?branch=master)](http://travis-ci.org/splitrb/split)
@@ -623,6 +623,35 @@ Once you finish one of the goals, the test is considered to be completed, and fi
623623

624624
**Bad Example**: Test both how button color affects signup *and* how it affects login, at the same time. THIS WILL NOT WORK.
625625

626+
#### Combined Experiments
627+
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
629+
```ruby
630+
Split.configuration.experiments = {
631+
:button_color_experiment => {
632+
:alternatives => ["blue", "green"],
633+
:combined_experiments => ["button_color_on_signup", "button_color_on_login"]
634+
}
635+
}
636+
```
637+
638+
Starting the combined test starts all combined experiments
639+
```ruby
640+
ab_combined_test(:button_color_experiment)
641+
```
642+
Finish each combined test as normal
643+
644+
```ruby
645+
ab_finished(:button_color_on_login)
646+
ab_finished(:button_color_on_signup)
647+
```
648+
649+
**Additional Configuration**:
650+
* Be sure to enable `allow_multiple_experiments`
651+
* In Sinatra include the CombinedExperimentsHelper
652+
```
653+
helpers Split::CombinedExperimentsHelper
654+
```
626655
### DB failover solution
627656

628657
Due to the fact that Redis has no automatic failover mechanism, it's

lib/split.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
require 'split/extensions/string'
1414
require 'split/goals_collection'
1515
require 'split/helper'
16+
require 'split/combined_experiments_helper'
1617
require 'split/metric'
1718
require 'split/persistence'
1819
require 'split/redis_interface'
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# frozen_string_literal: true
2+
module Split
3+
module CombinedExperimentsHelper
4+
def ab_combined_test(metric_descriptor, control = nil, *alternatives)
5+
return nil unless experiment = find_combined_experiment(metric_descriptor)
6+
raise(Split::InvalidExperimentsFormatError, 'Unable to find experiment #{metric_descriptor} in configuration') if experiment[:combined_experiments].nil?
7+
8+
alternative = nil
9+
experiment[:combined_experiments].each do |combined_experiment|
10+
if alternative.nil?
11+
if control
12+
alternative = ab_test(combined_experiment, control, alternatives)
13+
else
14+
normalized_alternatives = Split::Configuration.new.normalize_alternatives(experiment[:alternatives])
15+
alternative = ab_test(combined_experiment, normalized_alternatives[0], *normalized_alternatives[1])
16+
end
17+
else
18+
ab_test(combined_experiment, [{alternative => 1}])
19+
end
20+
end
21+
end
22+
23+
def find_combined_experiment(metric_descriptor)
24+
raise(Split::InvalidExperimentsFormatError, 'Invalid descriptor class (String or Symbol required)') unless metric_descriptor.class == String || metric_descriptor.class == Symbol
25+
raise(Split::InvalidExperimentsFormatError, 'Enable configuration') unless Split.configuration.enabled
26+
raise(Split::InvalidExperimentsFormatError, 'Enable `allow_multiple_experiments`') unless Split.configuration.allow_multiple_experiments
27+
experiment = Split::configuration.experiments[metric_descriptor.to_sym]
28+
end
29+
end
30+
end

lib/split/engine.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ class Engine < ::Rails::Engine
55
if Split.configuration.include_rails_helper
66
ActionController::Base.send :include, Split::Helper
77
ActionController::Base.helper Split::Helper
8+
ActionController::Base.send :include, Split::CombinedExperimentsHelper
9+
ActionController::Base.helper Split::CombinedExperimentsHelper
810
end
911
end
1012
end

lib/split/helper.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ def ab_test(metric_descriptor, control = nil, *alternatives)
1010
experiment = ExperimentCatalog.find_or_initialize(metric_descriptor, control, *alternatives)
1111
alternative = if Split.configuration.enabled
1212
experiment.save
13+
raise(Split::InvalidExperimentsFormatError) unless Split::configuration.experiments&.dig(experiment.name.to_sym,:combined_experiments).nil?
1314
trial = Trial.new(:user => ab_user, :experiment => experiment,
1415
:override => override_alternative(experiment.name), :exclude => exclude_visitor?,
1516
:disabled => split_generically_disabled?)
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# frozen_string_literal: true
2+
require 'spec_helper'
3+
require 'split/combined_experiments_helper'
4+
5+
describe Split::CombinedExperimentsHelper do
6+
include Split::CombinedExperimentsHelper
7+
8+
describe 'ab_combined_test' do
9+
let!(:config_enabled) { true }
10+
let!(:combined_experiments) { [:exp_1_click, :exp_1_scroll ]}
11+
let!(:allow_multiple_experiments) { true }
12+
13+
before do
14+
Split.configuration.experiments = {
15+
:combined_exp_1 => {
16+
:alternatives => [ {"control"=> 0.5}, {"test-alt"=> 0.5} ],
17+
:metric => :my_metric,
18+
:combined_experiments => combined_experiments
19+
}
20+
}
21+
Split.configuration.enabled = config_enabled
22+
Split.configuration.allow_multiple_experiments = allow_multiple_experiments
23+
end
24+
25+
context 'without config enabled' do
26+
let!(:config_enabled) { false }
27+
28+
it "raises an error" do
29+
expect(lambda { ab_combined_test :combined_exp_1 }).to raise_error(Split::InvalidExperimentsFormatError )
30+
end
31+
end
32+
33+
context 'multiple experiments disabled' do
34+
let!(:allow_multiple_experiments) { false }
35+
36+
it "raises an error if multiple experiments is disabled" do
37+
expect(lambda { ab_combined_test :combined_exp_1 }).to raise_error(Split::InvalidExperimentsFormatError)
38+
end
39+
end
40+
41+
context 'without combined experiments' do
42+
let!(:combined_experiments) { nil }
43+
44+
it "raises an error" do
45+
expect(lambda { ab_combined_test :combined_exp_1 }).to raise_error(Split::InvalidExperimentsFormatError )
46+
end
47+
end
48+
49+
it "uses same alternatives for all sub experiments " do
50+
allow(self).to receive(:get_alternative) { "test-alt" }
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}] )
53+
54+
ab_combined_test('combined_exp_1')
55+
end
56+
end
57+
end

spec/helper_spec.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,18 @@
3535
expect(lambda { ab_test({'link_color' => "purchase"}, 'blue', 'red') }).not_to raise_error
3636
end
3737

38+
it "raises an appropriate error when processing combined expirements" do
39+
Split.configuration.experiments = {
40+
:combined_exp_1 => {
41+
:alternatives => [ { name: "control", percent: 50 }, { name: "test-alt", percent: 50 } ],
42+
:metric => :my_metric,
43+
:combined_experiments => [:combined_exp_1_sub_1]
44+
}
45+
}
46+
Split::ExperimentCatalog.find_or_create('combined_exp_1')
47+
expect(lambda { ab_test('combined_exp_1')}).to raise_error(Split::InvalidExperimentsFormatError )
48+
end
49+
3850
it "should assign a random alternative to a new user when there are an equal number of alternatives assigned" do
3951
ab_test('link_color', 'blue', 'red')
4052
expect(['red', 'blue']).to include(ab_user['link_color'])

0 commit comments

Comments
 (0)