split 2.2.0 → 3.1.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 +4 -4
- data/.travis.yml +1 -2
- data/Appraisals +6 -5
- data/CHANGELOG.md +38 -0
- data/CONTRIBUTING.md +54 -5
- data/LICENSE +1 -1
- data/README.md +153 -112
- data/gemfiles/4.2.gemfile +1 -1
- data/gemfiles/5.0.gemfile +2 -2
- data/gemfiles/{4.1.gemfile → 5.1.gemfile} +3 -2
- data/lib/split/algorithms/block_randomization.rb +22 -0
- data/lib/split/alternative.rb +29 -7
- data/lib/split/combined_experiments_helper.rb +30 -0
- data/lib/split/configuration.rb +5 -0
- data/lib/split/dashboard/helpers.rb +5 -1
- data/lib/split/dashboard/views/_experiment.erb +31 -1
- data/lib/split/encapsulated_helper.rb +2 -0
- data/lib/split/engine.rb +2 -0
- data/lib/split/helper.rb +29 -0
- data/lib/split/persistence/cookie_adapter.rb +19 -15
- data/lib/split/persistence/dual_adapter.rb +3 -0
- data/lib/split/persistence.rb +5 -3
- data/lib/split/user.rb +2 -0
- data/lib/split/version.rb +2 -2
- data/lib/split/zscore.rb +1 -1
- data/lib/split.rb +21 -19
- data/spec/algorithms/block_randomization_spec.rb +32 -0
- data/spec/alternative_spec.rb +31 -0
- data/spec/combined_experiments_helper_spec.rb +57 -0
- data/spec/dashboard_helpers_spec.rb +14 -0
- data/spec/experiment_spec.rb +0 -2
- data/spec/helper_spec.rb +12 -0
- data/spec/persistence/cookie_adapter_spec.rb +6 -2
- data/split.gemspec +11 -2
- metadata +20 -10
- data/lib/split/algorithms.rb +0 -4
- data/lib/split/extensions.rb +0 -4
data/lib/split/alternative.rb
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
-
require 'split/zscore'
|
|
3
|
-
|
|
4
|
-
# TODO - take out require and implement using file paths?
|
|
5
|
-
|
|
6
2
|
module Split
|
|
7
3
|
class Alternative
|
|
8
4
|
attr_accessor :name
|
|
9
5
|
attr_accessor :experiment_name
|
|
10
6
|
attr_accessor :weight
|
|
11
|
-
|
|
12
|
-
include Zscore
|
|
7
|
+
attr_accessor :recorded_info
|
|
13
8
|
|
|
14
9
|
def initialize(name, experiment_name)
|
|
15
10
|
@experiment_name = experiment_name
|
|
@@ -127,10 +122,37 @@ module Split
|
|
|
127
122
|
z_score = Split::Zscore.calculate(p_a, n_a, p_c, n_c)
|
|
128
123
|
end
|
|
129
124
|
|
|
125
|
+
def extra_info
|
|
126
|
+
data = Split.redis.hget(key, 'recorded_info')
|
|
127
|
+
if data && data.length > 1
|
|
128
|
+
begin
|
|
129
|
+
JSON.parse(data)
|
|
130
|
+
rescue
|
|
131
|
+
{}
|
|
132
|
+
end
|
|
133
|
+
else
|
|
134
|
+
{}
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def record_extra_info(k, value = 1)
|
|
139
|
+
@recorded_info = self.extra_info || {}
|
|
140
|
+
|
|
141
|
+
if value.kind_of?(Numeric)
|
|
142
|
+
@recorded_info[k] ||= 0
|
|
143
|
+
@recorded_info[k] += value
|
|
144
|
+
else
|
|
145
|
+
@recorded_info[k] = value
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
Split.redis.hset key, 'recorded_info', (@recorded_info || {}).to_json
|
|
149
|
+
end
|
|
150
|
+
|
|
130
151
|
def save
|
|
131
152
|
Split.redis.hsetnx key, 'participant_count', 0
|
|
132
153
|
Split.redis.hsetnx key, 'completed_count', 0
|
|
133
154
|
Split.redis.hsetnx key, 'p_winner', p_winner
|
|
155
|
+
Split.redis.hsetnx key, 'recorded_info', (@recorded_info || {}).to_json
|
|
134
156
|
end
|
|
135
157
|
|
|
136
158
|
def validate!
|
|
@@ -140,7 +162,7 @@ module Split
|
|
|
140
162
|
end
|
|
141
163
|
|
|
142
164
|
def reset
|
|
143
|
-
Split.redis.hmset key, 'participant_count', 0, 'completed_count', 0
|
|
165
|
+
Split.redis.hmset key, 'participant_count', 0, 'completed_count', 0, 'recorded_info', nil
|
|
144
166
|
unless goals.empty?
|
|
145
167
|
goals.each do |g|
|
|
146
168
|
field = "completed_count:#{g}"
|
|
@@ -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
|
data/lib/split/configuration.rb
CHANGED
|
@@ -48,7 +48,9 @@ module Split
|
|
|
48
48
|
'spider' => 'generic web spider',
|
|
49
49
|
'UnwindFetchor' => 'Gnip crawler',
|
|
50
50
|
'WordPress' => 'WordPress spider',
|
|
51
|
+
'YandexAccessibilityBot' => 'Yandex accessibility spider',
|
|
51
52
|
'YandexBot' => 'Yandex spider',
|
|
53
|
+
'YandexMobileBot' => 'Yandex mobile spider',
|
|
52
54
|
'ZIBB' => 'ZIBB spider',
|
|
53
55
|
|
|
54
56
|
# HTTP libraries
|
|
@@ -73,10 +75,13 @@ module Split
|
|
|
73
75
|
'facebookexternalhit' => 'facebook bot',
|
|
74
76
|
'Feedfetcher-Google' => 'Google Feedfetcher',
|
|
75
77
|
'https://developers.google.com/+/web/snippet' => 'Google+ Snippet Fetcher',
|
|
78
|
+
'LinkedInBot' => 'LinkedIn bot',
|
|
76
79
|
'LongURL' => 'URL expander service',
|
|
77
80
|
'NING' => 'NING - Yet Another Twitter Swarmer',
|
|
81
|
+
'Pinterest' => 'Pinterest Bot',
|
|
78
82
|
'redditbot' => 'Reddit Bot',
|
|
79
83
|
'ShortLinkTranslate' => 'Link shortener',
|
|
84
|
+
'Slackbot' => 'Slackbot link expander',
|
|
80
85
|
'TweetmemeBot' => 'TweetMeMe Crawler',
|
|
81
86
|
'Twitterbot' => 'Twitter URL expander',
|
|
82
87
|
'UnwindFetch' => 'Gnip URL expander',
|
|
@@ -18,7 +18,11 @@ module Split
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def round(number, precision = 2)
|
|
21
|
-
|
|
21
|
+
begin
|
|
22
|
+
BigDecimal.new(number.to_s)
|
|
23
|
+
rescue ArgumentError
|
|
24
|
+
BigDecimal.new(0)
|
|
25
|
+
end.round(precision).to_f
|
|
22
26
|
end
|
|
23
27
|
|
|
24
28
|
def confidence_level(z_score)
|
|
@@ -5,6 +5,25 @@
|
|
|
5
5
|
<% end %>
|
|
6
6
|
|
|
7
7
|
<% experiment.calc_winning_alternatives %>
|
|
8
|
+
<%
|
|
9
|
+
extra_columns = []
|
|
10
|
+
experiment.alternatives.each do |alternative|
|
|
11
|
+
extra_info = alternative.extra_info || {}
|
|
12
|
+
extra_columns += extra_info.keys
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
extra_columns.uniq!
|
|
16
|
+
summary_texts = {}
|
|
17
|
+
extra_columns.each do |column|
|
|
18
|
+
extra_infos = experiment.alternatives.map(&:extra_info).select{|extra_info| extra_info && extra_info[column] }
|
|
19
|
+
if extra_infos[0][column].kind_of?(Numeric)
|
|
20
|
+
summary_texts[column] = extra_infos.inject(0){|sum, extra_info| sum += extra_info[column]}
|
|
21
|
+
else
|
|
22
|
+
summary_texts[column] = "N/A"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
%>
|
|
26
|
+
|
|
8
27
|
|
|
9
28
|
<div class="<%= experiment_class %>" data-name="<%= experiment.name %>" data-complete="<%= experiment.has_winner? %>">
|
|
10
29
|
<div class="experiment-header">
|
|
@@ -32,6 +51,9 @@
|
|
|
32
51
|
<th>Non-finished</th>
|
|
33
52
|
<th>Completed</th>
|
|
34
53
|
<th>Conversion Rate</th>
|
|
54
|
+
<% extra_columns.each do |column| %>
|
|
55
|
+
<th><%= column %></th>
|
|
56
|
+
<% end %>
|
|
35
57
|
<th>
|
|
36
58
|
<form>
|
|
37
59
|
<select id="dropdown-<%=experiment.jstring(goal)%>" name="dropdown-<%=experiment.jstring(goal)%>">
|
|
@@ -82,6 +104,9 @@
|
|
|
82
104
|
});
|
|
83
105
|
});
|
|
84
106
|
</script>
|
|
107
|
+
<% extra_columns.each do |column| %>
|
|
108
|
+
<td><%= alternative.extra_info && alternative.extra_info[column] %></td>
|
|
109
|
+
<% end %>
|
|
85
110
|
<td>
|
|
86
111
|
<div class="box-<%=experiment.jstring(goal)%> confidence-<%=experiment.jstring(goal)%>">
|
|
87
112
|
<span title='z-score: <%= round(alternative.z_score(goal), 3) %>'><%= confidence_level(alternative.z_score(goal)) %></span>
|
|
@@ -90,7 +115,7 @@
|
|
|
90
115
|
<div class="box-<%=experiment.jstring(goal)%> probability-<%=experiment.jstring(goal)%>">
|
|
91
116
|
<span title="p_winner: <%= round(alternative.p_winner(goal), 3) %>"><%= number_to_percentage(round(alternative.p_winner(goal), 3)) %>%</span>
|
|
92
117
|
</div>
|
|
93
|
-
</td>
|
|
118
|
+
</td>
|
|
94
119
|
<td>
|
|
95
120
|
<% if experiment.has_winner? %>
|
|
96
121
|
<% if experiment.winner.name == alternative.name %>
|
|
@@ -118,6 +143,11 @@
|
|
|
118
143
|
<td><%= total_unfinished %></td>
|
|
119
144
|
<td><%= total_completed %></td>
|
|
120
145
|
<td>N/A</td>
|
|
146
|
+
<% extra_columns.each do |column| %>
|
|
147
|
+
<td>
|
|
148
|
+
<%= summary_texts[column] %>
|
|
149
|
+
</td>
|
|
150
|
+
<% end %>
|
|
121
151
|
<td>N/A</td>
|
|
122
152
|
<td>N/A</td>
|
|
123
153
|
</tr>
|
data/lib/split/engine.rb
CHANGED
|
@@ -5,6 +5,8 @@ module Split
|
|
|
5
5
|
if Split.configuration.include_rails_helper
|
|
6
6
|
ActionController::Base.send :include, Split::Helper
|
|
7
7
|
ActionController::Base.helper Split::Helper
|
|
8
|
+
ActionController::Base.send :include, Split::CombinedExperimentsHelper
|
|
9
|
+
ActionController::Base.helper Split::CombinedExperimentsHelper
|
|
8
10
|
end
|
|
9
11
|
end
|
|
10
12
|
end
|
data/lib/split/helper.rb
CHANGED
|
@@ -10,6 +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
14
|
trial = Trial.new(:user => ab_user, :experiment => experiment,
|
|
14
15
|
:override => override_alternative(experiment.name), :exclude => exclude_visitor?,
|
|
15
16
|
:disabled => split_generically_disabled?)
|
|
@@ -76,6 +77,34 @@ module Split
|
|
|
76
77
|
Split.configuration.db_failover_on_db_error.call(e)
|
|
77
78
|
end
|
|
78
79
|
|
|
80
|
+
def ab_record_extra_info(metric_descriptor, key, value = 1)
|
|
81
|
+
return if exclude_visitor? || Split.configuration.disabled?
|
|
82
|
+
metric_descriptor, goals = normalize_metric(metric_descriptor)
|
|
83
|
+
experiments = Metric.possible_experiments(metric_descriptor)
|
|
84
|
+
|
|
85
|
+
if experiments.any?
|
|
86
|
+
experiments.each do |experiment|
|
|
87
|
+
alternative_name = ab_user[experiment.key]
|
|
88
|
+
|
|
89
|
+
if alternative_name
|
|
90
|
+
alternative = experiment.alternatives.find{|alt| alt.name == alternative_name}
|
|
91
|
+
alternative.record_extra_info(key, value) if alternative
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
rescue => e
|
|
96
|
+
raise unless Split.configuration.db_failover
|
|
97
|
+
Split.configuration.db_failover_on_db_error.call(e)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def ab_active_experiments()
|
|
101
|
+
ab_user.active_experiments
|
|
102
|
+
rescue => e
|
|
103
|
+
raise unless Split.configuration.db_failover
|
|
104
|
+
Split.configuration.db_failover_on_db_error.call(e)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
|
|
79
108
|
def override_present?(experiment_name)
|
|
80
109
|
override_alternative(experiment_name)
|
|
81
110
|
end
|
|
@@ -6,20 +6,21 @@ module Split
|
|
|
6
6
|
class CookieAdapter
|
|
7
7
|
|
|
8
8
|
def initialize(context)
|
|
9
|
-
@
|
|
9
|
+
@request, @response = context.request, context.response
|
|
10
|
+
@cookies = @request.cookies
|
|
10
11
|
@expires = Time.now + cookie_length_config
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
def [](key)
|
|
14
|
-
hash[key]
|
|
15
|
+
hash[key.to_s]
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
def []=(key, value)
|
|
18
|
-
set_cookie(hash.merge(key => value))
|
|
19
|
+
set_cookie(hash.merge!(key.to_s => value))
|
|
19
20
|
end
|
|
20
21
|
|
|
21
22
|
def delete(key)
|
|
22
|
-
set_cookie(hash.tap { |h| h.delete(key) })
|
|
23
|
+
set_cookie(hash.tap { |h| h.delete(key.to_s) })
|
|
23
24
|
end
|
|
24
25
|
|
|
25
26
|
def keys
|
|
@@ -28,22 +29,25 @@ module Split
|
|
|
28
29
|
|
|
29
30
|
private
|
|
30
31
|
|
|
31
|
-
def set_cookie(value)
|
|
32
|
-
@
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
def set_cookie(value = {})
|
|
33
|
+
@response.set_cookie :split.to_s, default_options.merge(value: JSON.generate(value))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def default_options
|
|
37
|
+
{ expires: @expires, path: '/' }
|
|
36
38
|
end
|
|
37
39
|
|
|
38
40
|
def hash
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
@hash ||= begin
|
|
42
|
+
if cookies = @cookies[:split.to_s]
|
|
43
|
+
begin
|
|
44
|
+
JSON.parse(cookies)
|
|
45
|
+
rescue JSON::ParserError
|
|
46
|
+
{}
|
|
47
|
+
end
|
|
48
|
+
else
|
|
43
49
|
{}
|
|
44
50
|
end
|
|
45
|
-
else
|
|
46
|
-
{}
|
|
47
51
|
end
|
|
48
52
|
end
|
|
49
53
|
|
data/lib/split/persistence.rb
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
-
%w[session_adapter cookie_adapter redis_adapter dual_adapter].each do |f|
|
|
3
|
-
require "split/persistence/#{f}"
|
|
4
|
-
end
|
|
5
2
|
|
|
6
3
|
module Split
|
|
7
4
|
module Persistence
|
|
5
|
+
require 'split/persistence/cookie_adapter'
|
|
6
|
+
require 'split/persistence/dual_adapter'
|
|
7
|
+
require 'split/persistence/redis_adapter'
|
|
8
|
+
require 'split/persistence/session_adapter'
|
|
9
|
+
|
|
8
10
|
ADAPTERS = {
|
|
9
11
|
:cookie => Split::Persistence::CookieAdapter,
|
|
10
12
|
:session => Split::Persistence::SessionAdapter
|
data/lib/split/user.rb
CHANGED
data/lib/split/version.rb
CHANGED
data/lib/split/zscore.rb
CHANGED
data/lib/split.rb
CHANGED
|
@@ -1,24 +1,26 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
alternative
|
|
4
|
-
configuration
|
|
5
|
-
exceptions
|
|
6
|
-
experiment
|
|
7
|
-
experiment_catalog
|
|
8
|
-
extensions
|
|
9
|
-
goals_collection
|
|
10
|
-
helper
|
|
11
|
-
metric
|
|
12
|
-
persistence
|
|
13
|
-
encapsulated_helper
|
|
14
|
-
redis_interface
|
|
15
|
-
trial
|
|
16
|
-
user
|
|
17
|
-
version
|
|
18
|
-
zscore].each do |f|
|
|
19
|
-
require "split/#{f}"
|
|
20
|
-
end
|
|
2
|
+
require 'redis'
|
|
21
3
|
|
|
4
|
+
require 'split/algorithms/block_randomization'
|
|
5
|
+
require 'split/algorithms/weighted_sample'
|
|
6
|
+
require 'split/algorithms/whiplash'
|
|
7
|
+
require 'split/alternative'
|
|
8
|
+
require 'split/configuration'
|
|
9
|
+
require 'split/encapsulated_helper'
|
|
10
|
+
require 'split/exceptions'
|
|
11
|
+
require 'split/experiment'
|
|
12
|
+
require 'split/experiment_catalog'
|
|
13
|
+
require 'split/extensions/string'
|
|
14
|
+
require 'split/goals_collection'
|
|
15
|
+
require 'split/helper'
|
|
16
|
+
require 'split/combined_experiments_helper'
|
|
17
|
+
require 'split/metric'
|
|
18
|
+
require 'split/persistence'
|
|
19
|
+
require 'split/redis_interface'
|
|
20
|
+
require 'split/trial'
|
|
21
|
+
require 'split/user'
|
|
22
|
+
require 'split/version'
|
|
23
|
+
require 'split/zscore'
|
|
22
24
|
require 'split/engine' if defined?(Rails)
|
|
23
25
|
|
|
24
26
|
module Split
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
|
|
3
|
+
describe Split::Algorithms::BlockRandomization do
|
|
4
|
+
|
|
5
|
+
let(:experiment) { Split::Experiment.new 'experiment' }
|
|
6
|
+
let(:alternative_A) { Split::Alternative.new 'A', 'experiment' }
|
|
7
|
+
let(:alternative_B) { Split::Alternative.new 'B', 'experiment' }
|
|
8
|
+
let(:alternative_C) { Split::Alternative.new 'C', 'experiment' }
|
|
9
|
+
|
|
10
|
+
before :each do
|
|
11
|
+
allow(experiment).to receive(:alternatives) { [alternative_A, alternative_B, alternative_C] }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it "should return an alternative" do
|
|
15
|
+
expect(Split::Algorithms::BlockRandomization.choose_alternative(experiment).class).to eq(Split::Alternative)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it "should always return the minimum participation option" do
|
|
19
|
+
allow(alternative_A).to receive(:participant_count) { 1 }
|
|
20
|
+
allow(alternative_B).to receive(:participant_count) { 1 }
|
|
21
|
+
allow(alternative_C).to receive(:participant_count) { 0 }
|
|
22
|
+
expect(Split::Algorithms::BlockRandomization.choose_alternative(experiment)).to eq(alternative_C)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "should return one of the minimum participation options when multiple" do
|
|
26
|
+
allow(alternative_A).to receive(:participant_count) { 0 }
|
|
27
|
+
allow(alternative_B).to receive(:participant_count) { 0 }
|
|
28
|
+
allow(alternative_C).to receive(:participant_count) { 0 }
|
|
29
|
+
alternative = Split::Algorithms::BlockRandomization.choose_alternative(experiment)
|
|
30
|
+
expect([alternative_A, alternative_B, alternative_C].include?(alternative)).to be(true)
|
|
31
|
+
end
|
|
32
|
+
end
|
data/spec/alternative_spec.rb
CHANGED
|
@@ -274,4 +274,35 @@ describe Split::Alternative do
|
|
|
274
274
|
expect(control.z_score(goal2)).to eq('N/A')
|
|
275
275
|
end
|
|
276
276
|
end
|
|
277
|
+
|
|
278
|
+
describe "extra_info" do
|
|
279
|
+
it "reads saved value of recorded_info in redis" do
|
|
280
|
+
saved_recorded_info = {"key_1" => 1, "key_2" => "2"}
|
|
281
|
+
Split.redis.hset "#{alternative.experiment_name}:#{alternative.name}", 'recorded_info', saved_recorded_info.to_json
|
|
282
|
+
extra_info = alternative.extra_info
|
|
283
|
+
|
|
284
|
+
expect(extra_info).to eql(saved_recorded_info)
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
describe "record_extra_info" do
|
|
289
|
+
it "saves key" do
|
|
290
|
+
alternative.record_extra_info("signup", 1)
|
|
291
|
+
expect(alternative.extra_info["signup"]).to eql(1)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
it "adds value to saved key's value second argument is number" do
|
|
295
|
+
alternative.record_extra_info("signup", 1)
|
|
296
|
+
alternative.record_extra_info("signup", 2)
|
|
297
|
+
expect(alternative.extra_info["signup"]).to eql(3)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
it "sets saved's key value to the second argument if it's a string" do
|
|
301
|
+
alternative.record_extra_info("signup", "Value 1")
|
|
302
|
+
expect(alternative.extra_info["signup"]).to eql("Value 1")
|
|
303
|
+
|
|
304
|
+
alternative.record_extra_info("signup", "Value 2")
|
|
305
|
+
expect(alternative.extra_info["signup"]).to eql("Value 2")
|
|
306
|
+
end
|
|
307
|
+
end
|
|
277
308
|
end
|
|
@@ -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
|
|
@@ -24,5 +24,19 @@ describe Split::DashboardHelpers do
|
|
|
24
24
|
expect(confidence_level(2.58)).to eq('99% confidence')
|
|
25
25
|
expect(confidence_level(3.00)).to eq('99% confidence')
|
|
26
26
|
end
|
|
27
|
+
|
|
28
|
+
describe '#round' do
|
|
29
|
+
it 'can round number strings' do
|
|
30
|
+
expect(round('3.1415')).to eq BigDecimal.new('3.14')
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'can round number strings for precsion' do
|
|
34
|
+
expect(round('3.1415', 1)).to eq BigDecimal.new('3.1')
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'can handle invalid number strings' do
|
|
38
|
+
expect(round('N/A')).to be_zero
|
|
39
|
+
end
|
|
40
|
+
end
|
|
27
41
|
end
|
|
28
42
|
end
|
data/spec/experiment_spec.rb
CHANGED
data/spec/helper_spec.rb
CHANGED
|
@@ -35,6 +35,18 @@ describe Split::Helper do
|
|
|
35
35
|
expect(lambda { ab_test({'link_color' => "purchase"}, 'blue', 'red') }).not_to raise_error
|
|
36
36
|
end
|
|
37
37
|
|
|
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
|
+
|
|
38
50
|
it "should assign a random alternative to a new user when there are an equal number of alternatives assigned" do
|
|
39
51
|
ab_test('link_color', 'blue', 'red')
|
|
40
52
|
expect(['red', 'blue']).to include(ab_user['link_color'])
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
require "spec_helper"
|
|
3
|
+
require 'rack/test'
|
|
3
4
|
|
|
4
5
|
describe Split::Persistence::CookieAdapter do
|
|
5
6
|
|
|
6
|
-
let(:
|
|
7
|
+
let(:env) { Rack::MockRequest.env_for("http://example.com:8080/") }
|
|
8
|
+
let(:request) { Rack::Request.new(env) }
|
|
9
|
+
let(:response) { Rack::MockResponse.new(200, {}, "") }
|
|
10
|
+
let(:context) { double(request: request, response: response) }
|
|
7
11
|
subject { Split::Persistence::CookieAdapter.new(context) }
|
|
8
12
|
|
|
9
13
|
describe "#[] and #[]=" do
|
|
@@ -30,7 +34,7 @@ describe Split::Persistence::CookieAdapter do
|
|
|
30
34
|
end
|
|
31
35
|
|
|
32
36
|
it "handles invalid JSON" do
|
|
33
|
-
context.cookies[:split] = { :value => '{"foo":2,', :expires => Time.now }
|
|
37
|
+
context.request.cookies[:split] = { :value => '{"foo":2,', :expires => Time.now }
|
|
34
38
|
expect(subject["my_key"]).to be_nil
|
|
35
39
|
subject["my_key"] = "my_value"
|
|
36
40
|
expect(subject["my_key"]).to eq("my_value")
|
data/split.gemspec
CHANGED
|
@@ -12,6 +12,15 @@ Gem::Specification.new do |s|
|
|
|
12
12
|
s.homepage = "https://github.com/splitrb/split"
|
|
13
13
|
s.summary = "Rack based split testing framework"
|
|
14
14
|
|
|
15
|
+
s.metadata = {
|
|
16
|
+
"homepage_uri" => "https://github.com/splitrb/split",
|
|
17
|
+
"changelog_uri" => "https://github.com/splitrb/split/blob/master/CHANGELOG.md",
|
|
18
|
+
"source_code_uri" => "https://github.com/splitrb/split",
|
|
19
|
+
"bug_tracker_uri" => "https://github.com/splitrb/split/issues",
|
|
20
|
+
"wiki_uri" => "https://github.com/splitrb/split/wiki",
|
|
21
|
+
"mailing_list_uri" => "https://groups.google.com/d/forum/split-ruby"
|
|
22
|
+
}
|
|
23
|
+
|
|
15
24
|
s.required_ruby_version = '>= 1.9.2'
|
|
16
25
|
|
|
17
26
|
s.rubyforge_project = "split"
|
|
@@ -24,10 +33,10 @@ Gem::Specification.new do |s|
|
|
|
24
33
|
s.add_dependency 'sinatra', '>= 1.2.6'
|
|
25
34
|
s.add_dependency 'simple-random', '>= 0.9.3'
|
|
26
35
|
|
|
27
|
-
s.add_development_dependency 'bundler', '~> 1.
|
|
36
|
+
s.add_development_dependency 'bundler', '~> 1.14'
|
|
28
37
|
s.add_development_dependency 'simplecov', '~> 0.12'
|
|
29
38
|
s.add_development_dependency 'rack-test', '~> 0.6'
|
|
30
|
-
s.add_development_dependency 'rake', '~>
|
|
39
|
+
s.add_development_dependency 'rake', '~> 12'
|
|
31
40
|
s.add_development_dependency 'rspec', '~> 3.4'
|
|
32
41
|
s.add_development_dependency 'pry', '~> 0.10'
|
|
33
42
|
s.add_development_dependency 'fakeredis', '~> 0.6.0'
|