split 2.2.0 → 3.3.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 (50) hide show
  1. checksums.yaml +5 -5
  2. data/.rubocop.yml +2 -2
  3. data/.travis.yml +39 -3
  4. data/Appraisals +8 -5
  5. data/CHANGELOG.md +59 -0
  6. data/CONTRIBUTING.md +54 -5
  7. data/LICENSE +1 -1
  8. data/README.md +193 -113
  9. data/gemfiles/4.2.gemfile +1 -1
  10. data/gemfiles/5.0.gemfile +1 -2
  11. data/gemfiles/{4.1.gemfile → 5.1.gemfile} +2 -2
  12. data/gemfiles/5.2.gemfile +9 -0
  13. data/lib/split/algorithms/block_randomization.rb +22 -0
  14. data/lib/split/alternative.rb +32 -7
  15. data/lib/split/combined_experiments_helper.rb +37 -0
  16. data/lib/split/configuration.rb +8 -0
  17. data/lib/split/dashboard/helpers.rb +5 -1
  18. data/lib/split/dashboard/pagination_helpers.rb +87 -0
  19. data/lib/split/dashboard/paginator.rb +16 -0
  20. data/lib/split/dashboard/public/style.css +9 -0
  21. data/lib/split/dashboard/views/_experiment.erb +31 -1
  22. data/lib/split/dashboard/views/index.erb +5 -1
  23. data/lib/split/dashboard.rb +2 -0
  24. data/lib/split/encapsulated_helper.rb +2 -0
  25. data/lib/split/engine.rb +2 -0
  26. data/lib/split/experiment.rb +5 -4
  27. data/lib/split/helper.rb +34 -1
  28. data/lib/split/persistence/cookie_adapter.rb +53 -15
  29. data/lib/split/persistence/dual_adapter.rb +3 -0
  30. data/lib/split/persistence.rb +5 -3
  31. data/lib/split/redis_interface.rb +1 -3
  32. data/lib/split/user.rb +2 -0
  33. data/lib/split/version.rb +2 -2
  34. data/lib/split/zscore.rb +1 -1
  35. data/lib/split.rb +21 -19
  36. data/spec/algorithms/block_randomization_spec.rb +32 -0
  37. data/spec/alternative_spec.rb +43 -0
  38. data/spec/combined_experiments_helper_spec.rb +57 -0
  39. data/spec/dashboard/pagination_helpers_spec.rb +198 -0
  40. data/spec/dashboard/paginator_spec.rb +37 -0
  41. data/spec/dashboard_helpers_spec.rb +14 -0
  42. data/spec/experiment_spec.rb +1 -3
  43. data/spec/helper_spec.rb +20 -0
  44. data/spec/persistence/cookie_adapter_spec.rb +90 -23
  45. data/spec/persistence/dual_adapter_spec.rb +2 -2
  46. data/spec/split_spec.rb +7 -7
  47. data/split.gemspec +16 -6
  48. metadata +36 -19
  49. data/lib/split/algorithms.rb +0 -4
  50. data/lib/split/extensions.rb +0 -4
@@ -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
@@ -48,7 +49,9 @@ module Split
48
49
  'spider' => 'generic web spider',
49
50
  'UnwindFetchor' => 'Gnip crawler',
50
51
  'WordPress' => 'WordPress spider',
52
+ 'YandexAccessibilityBot' => 'Yandex accessibility spider',
51
53
  'YandexBot' => 'Yandex spider',
54
+ 'YandexMobileBot' => 'Yandex mobile spider',
52
55
  'ZIBB' => 'ZIBB spider',
53
56
 
54
57
  # HTTP libraries
@@ -71,12 +74,16 @@ module Split
71
74
  'bitlybot' => 'bit.ly bot',
72
75
  'bot@linkfluence.net' => 'Linkfluence bot',
73
76
  'facebookexternalhit' => 'facebook bot',
77
+ 'Facebot' => 'Facebook crawler',
74
78
  'Feedfetcher-Google' => 'Google Feedfetcher',
75
79
  'https://developers.google.com/+/web/snippet' => 'Google+ Snippet Fetcher',
80
+ 'LinkedInBot' => 'LinkedIn bot',
76
81
  'LongURL' => 'URL expander service',
77
82
  'NING' => 'NING - Yet Another Twitter Swarmer',
83
+ 'Pinterest' => 'Pinterest Bot',
78
84
  'redditbot' => 'Reddit Bot',
79
85
  'ShortLinkTranslate' => 'Link shortener',
86
+ 'Slackbot' => 'Slackbot link expander',
80
87
  'TweetmemeBot' => 'TweetMeMe Crawler',
81
88
  'Twitterbot' => 'Twitter URL expander',
82
89
  'UnwindFetch' => 'Gnip URL expander',
@@ -212,6 +219,7 @@ module Split
212
219
  @algorithm = Split::Algorithms::WeightedSample
213
220
  @include_rails_helper = true
214
221
  @beta_probability_simulations = 10000
222
+ @winning_alternative_recalculation_interval = 60 * 60 * 24 # 1 day
215
223
  @redis = ENV.fetch(ENV.fetch('REDIS_PROVIDER', 'REDIS_URL'), 'redis://localhost:6379')
216
224
  end
217
225
 
@@ -18,7 +18,11 @@ module Split
18
18
  end
19
19
 
20
20
  def round(number, precision = 2)
21
- BigDecimal.new(number.to_s).round(precision).to_f
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)
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+ require 'split/dashboard/paginator'
3
+
4
+ module Split
5
+ module DashboardPaginationHelpers
6
+ DEFAULT_PER = 10
7
+
8
+ def pagination_per
9
+ @pagination_per ||= (params[:per] || DEFAULT_PER).to_i
10
+ end
11
+
12
+ def page_number
13
+ @page_number ||= (params[:page] || 1).to_i
14
+ end
15
+
16
+ def paginated(collection)
17
+ Split::DashboardPaginator.new(collection, page_number, pagination_per).paginate
18
+ end
19
+
20
+ def pagination(collection)
21
+ html = []
22
+ html << first_page_tag if show_first_page_tag?
23
+ html << ellipsis_tag if show_first_ellipsis_tag?
24
+ html << prev_page_tag if show_prev_page_tag?
25
+ html << current_page_tag
26
+ html << next_page_tag if show_next_page_tag?(collection)
27
+ html << ellipsis_tag if show_last_ellipsis_tag?(collection)
28
+ html << last_page_tagcollection if show_last_page_tag?(collection)
29
+ html.join
30
+ end
31
+
32
+ private
33
+
34
+ def show_first_page_tag?
35
+ page_number > 2
36
+ end
37
+
38
+ def first_page_tag
39
+ %Q(<a href="#{url.chop}?page=1&per=#{pagination_per}">1</a>)
40
+ end
41
+
42
+ def show_first_ellipsis_tag?
43
+ page_number >= 4
44
+ end
45
+
46
+ def ellipsis_tag
47
+ '<span>...</span>'
48
+ end
49
+
50
+ def show_prev_page_tag?
51
+ page_number > 1
52
+ end
53
+
54
+ def prev_page_tag
55
+ %Q(<a href="#{url.chop}?page=#{page_number - 1}&per=#{pagination_per}">#{page_number - 1}</a>)
56
+ end
57
+
58
+ def current_page_tag
59
+ "<span><b>#{page_number}</b></span>"
60
+ end
61
+
62
+ def show_next_page_tag?(collection)
63
+ (page_number * pagination_per) < collection.count
64
+ end
65
+
66
+ def next_page_tag
67
+ %Q(<a href="#{url.chop}?page=#{page_number + 1}&per=#{pagination_per}">#{page_number + 1}</a>)
68
+ end
69
+
70
+ def show_last_ellipsis_tag?(collection)
71
+ (total_pages(collection) - page_number) >= 3
72
+ end
73
+
74
+ def total_pages(collection)
75
+ collection.count / pagination_per + ((collection.count % pagination_per).zero? ? 0 : 1)
76
+ end
77
+
78
+ def show_last_page_tag?(collection)
79
+ page_number < (total_pages(collection) - 1)
80
+ end
81
+
82
+ def last_page_tag(collection)
83
+ total = total_pages(collection)
84
+ %Q(<a href="#{url.chop}?page=#{total}&per=#{pagination_per}">#{total}</a>)
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+ module Split
3
+ class DashboardPaginator
4
+ def initialize(collection, page_number, per)
5
+ @collection = collection
6
+ @page_number = page_number
7
+ @per = per
8
+ end
9
+
10
+ def paginate
11
+ to = @page_number * @per
12
+ from = to - @per
13
+ @collection[from...to]
14
+ end
15
+ end
16
+ end
@@ -317,3 +317,12 @@ a.button.green:focus, button.green:focus, input[type="submit"].green:focus {
317
317
  }
318
318
 
319
319
 
320
+ .pagination {
321
+ text-align: center;
322
+ font-size: 15px;
323
+ }
324
+
325
+ .pagination a, .paginaton span {
326
+ display: inline-block;
327
+ padding: 5px;
328
+ }
@@ -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>
@@ -6,7 +6,7 @@
6
6
  <input type="button" id="toggle-active" value="Hide active" />
7
7
  <input type="button" id="clear-filter" value="Clear filters" />
8
8
 
9
- <% @experiments.each do |experiment| %>
9
+ <% paginated(@experiments).each do |experiment| %>
10
10
  <% if experiment.goals.empty? %>
11
11
  <%= erb :_experiment, :locals => {:goal => nil, :experiment => experiment} %>
12
12
  <% else %>
@@ -16,6 +16,10 @@
16
16
  <% end %>
17
17
  <% end %>
18
18
  <% end %>
19
+
20
+ <div class="pagination">
21
+ <%= pagination(@experiments) %>
22
+ </div>
19
23
  <% else %>
20
24
  <p class="intro">No experiments have started yet, you need to define them in your code and introduce them to your users.</p>
21
25
  <p class="intro">Check out the <a href='https://github.com/splitrb/split#readme'>Readme</a> for more help getting started.</p>
@@ -3,6 +3,7 @@ require 'sinatra/base'
3
3
  require 'split'
4
4
  require 'bigdecimal'
5
5
  require 'split/dashboard/helpers'
6
+ require 'split/dashboard/pagination_helpers'
6
7
 
7
8
  module Split
8
9
  class Dashboard < Sinatra::Base
@@ -14,6 +15,7 @@ module Split
14
15
  set :method_override, true
15
16
 
16
17
  helpers Split::DashboardHelpers
18
+ helpers Split::DashboardPaginationHelpers
17
19
 
18
20
  get '/' do
19
21
  # Display experiments without a winner at the top of the dashboard
@@ -1,4 +1,6 @@
1
1
  # frozen_string_literal: true
2
+ require "split/helper"
3
+
2
4
  # Split's helper exposes all kinds of methods we don't want to
3
5
  # mix into our model classes.
4
6
  #
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
@@ -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,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 || {}).fetch(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
@@ -93,13 +122,17 @@ module Split
93
122
  end
94
123
 
95
124
  def exclude_visitor?
96
- instance_eval(&Split.configuration.ignore_filter) || is_ignored_ip_address? || is_robot?
125
+ instance_exec(request, &Split.configuration.ignore_filter) || is_ignored_ip_address? || is_robot? || is_preview?
97
126
  end
98
127
 
99
128
  def is_robot?
100
129
  defined?(request) && request.user_agent =~ Split.configuration.robot_regex
101
130
  end
102
131
 
132
+ def is_preview?
133
+ defined?(request) && defined?(request.headers) && request.headers['x-purpose'] == 'preview'
134
+ end
135
+
103
136
  def is_ignored_ip_address?
104
137
  return false if Split.configuration.ignore_ip_addresses.empty?
105
138
 
@@ -6,20 +6,22 @@ module Split
6
6
  class CookieAdapter
7
7
 
8
8
  def initialize(context)
9
- @cookies = context.send(:cookies)
9
+ @context = context
10
+ @request, @response = context.request, context.response
11
+ @cookies = @request.cookies
10
12
  @expires = Time.now + cookie_length_config
11
13
  end
12
14
 
13
15
  def [](key)
14
- hash[key]
16
+ hash[key.to_s]
15
17
  end
16
18
 
17
19
  def []=(key, value)
18
- set_cookie(hash.merge(key => value))
20
+ set_cookie(hash.merge!(key.to_s => value))
19
21
  end
20
22
 
21
23
  def delete(key)
22
- set_cookie(hash.tap { |h| h.delete(key) })
24
+ set_cookie(hash.tap { |h| h.delete(key.to_s) })
23
25
  end
24
26
 
25
27
  def keys
@@ -28,22 +30,55 @@ module Split
28
30
 
29
31
  private
30
32
 
31
- def set_cookie(value)
32
- @cookies[:split] = {
33
- :value => JSON.generate(value),
34
- :expires => @expires
35
- }
33
+ def set_cookie(value = {})
34
+ cookie_key = :split.to_s
35
+ cookie_value = default_options.merge(value: JSON.generate(value))
36
+ if action_dispatch?
37
+ # The "send" is necessary when we call ab_test from the controller
38
+ # and thus @context is a rails controller, because then "cookies" is
39
+ # a private method.
40
+ @context.send(:cookies)[cookie_key] = cookie_value
41
+ else
42
+ set_cookie_via_rack(cookie_key, cookie_value)
43
+ end
44
+ end
45
+
46
+ def default_options
47
+ { expires: @expires, path: '/' }
48
+ end
49
+
50
+ def set_cookie_via_rack(key, value)
51
+ delete_cookie_header!(@response.header, key, value)
52
+ Rack::Utils.set_cookie_header!(@response.header, key, value)
53
+ end
54
+
55
+ # Use Rack::Utils#make_delete_cookie_header after Rack 2.0.0
56
+ def delete_cookie_header!(header, key, value)
57
+ cookie_header = header['Set-Cookie']
58
+ case cookie_header
59
+ when nil, ''
60
+ cookies = []
61
+ when String
62
+ cookies = cookie_header.split("\n")
63
+ when Array
64
+ cookies = cookie_header
65
+ end
66
+
67
+ cookies.reject! { |cookie| cookie =~ /\A#{Rack::Utils.escape(key)}=/ }
68
+ header['Set-Cookie'] = cookies.join("\n")
36
69
  end
37
70
 
38
71
  def hash
39
- if @cookies[:split]
40
- begin
41
- JSON.parse(@cookies[:split])
42
- rescue JSON::ParserError
72
+ @hash ||= begin
73
+ if cookies = @cookies[:split.to_s]
74
+ begin
75
+ JSON.parse(cookies)
76
+ rescue JSON::ParserError
77
+ {}
78
+ end
79
+ else
43
80
  {}
44
81
  end
45
- else
46
- {}
47
82
  end
48
83
  end
49
84
 
@@ -51,6 +86,9 @@ module Split
51
86
  Split.configuration.persistence_cookie_length
52
87
  end
53
88
 
89
+ def action_dispatch?
90
+ defined?(Rails) && @response.is_a?(ActionDispatch::Response)
91
+ end
54
92
  end
55
93
  end
56
94
  end
@@ -1,4 +1,7 @@
1
1
  # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
2
5
  module Split
3
6
  module Persistence
4
7
  class DualAdapter
@@ -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
@@ -35,9 +35,7 @@ module Split
35
35
  end
36
36
 
37
37
  def make_list_length(list_name, new_length)
38
- while list_length(list_name) > new_length
39
- remove_last_item_from_list(list_name)
40
- end
38
+ redis.ltrim(list_name, 0, new_length - 1)
41
39
  end
42
40
 
43
41
  def add_to_set(set_name, value)
data/lib/split/user.rb CHANGED
@@ -1,3 +1,5 @@
1
+ require 'forwardable'
2
+
1
3
  module Split
2
4
  class User
3
5
  extend Forwardable
data/lib/split/version.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  module Split
3
- MAJOR = 2
4
- MINOR = 2
3
+ MAJOR = 3
4
+ MINOR = 3
5
5
  PATCH = 0
6
6
  VERSION = [MAJOR, MINOR, PATCH].join('.')
7
7
  end
data/lib/split/zscore.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  module Split
3
- module Zscore
3
+ class Zscore
4
4
 
5
5
  include Math
6
6
 
data/lib/split.rb CHANGED
@@ -1,24 +1,26 @@
1
1
  # frozen_string_literal: true
2
- %w[algorithms
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
@@ -273,5 +273,48 @@ 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
288
+ end
289
+
290
+ describe "extra_info" do
291
+ it "reads saved value of recorded_info in redis" do
292
+ saved_recorded_info = {"key_1" => 1, "key_2" => "2"}
293
+ Split.redis.hset "#{alternative.experiment_name}:#{alternative.name}", 'recorded_info', saved_recorded_info.to_json
294
+ extra_info = alternative.extra_info
295
+
296
+ expect(extra_info).to eql(saved_recorded_info)
297
+ end
298
+ end
299
+
300
+ describe "record_extra_info" do
301
+ it "saves key" do
302
+ alternative.record_extra_info("signup", 1)
303
+ expect(alternative.extra_info["signup"]).to eql(1)
304
+ end
305
+
306
+ it "adds value to saved key's value second argument is number" do
307
+ alternative.record_extra_info("signup", 1)
308
+ alternative.record_extra_info("signup", 2)
309
+ expect(alternative.extra_info["signup"]).to eql(3)
310
+ end
311
+
312
+ it "sets saved's key value to the second argument if it's a string" do
313
+ alternative.record_extra_info("signup", "Value 1")
314
+ expect(alternative.extra_info["signup"]).to eql("Value 1")
315
+
316
+ alternative.record_extra_info("signup", "Value 2")
317
+ expect(alternative.extra_info["signup"]).to eql("Value 2")
318
+ end
276
319
  end
277
320
  end