split 2.0.0 → 3.0.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/.codeclimate.yml +30 -0
- data/.csslintrc +2 -0
- data/.eslintignore +1 -0
- data/.eslintrc +213 -0
- data/.rubocop.yml +1156 -0
- data/.travis.yml +5 -2
- data/Appraisals +5 -4
- data/CHANGELOG.md +46 -0
- data/Gemfile +1 -0
- data/README.md +123 -20
- data/gemfiles/4.2.gemfile +1 -0
- data/gemfiles/5.0.gemfile +10 -0
- data/lib/split/algorithms/block_randomization.rb +22 -0
- data/lib/split/alternative.rb +29 -8
- data/lib/split/configuration.rb +13 -2
- data/lib/split/dashboard/helpers.rb +5 -1
- data/lib/split/dashboard/public/dashboard-filtering.js +3 -3
- data/lib/split/dashboard/views/_experiment.erb +35 -1
- data/lib/split/dashboard.rb +5 -0
- data/lib/split/encapsulated_helper.rb +4 -15
- data/lib/split/experiment.rb +63 -54
- data/lib/split/goals_collection.rb +1 -1
- data/lib/split/helper.rb +20 -0
- data/lib/split/persistence/dual_adapter.rb +3 -0
- data/lib/split/persistence.rb +5 -3
- data/lib/split/redis_interface.rb +51 -0
- data/lib/split/user.rb +3 -1
- data/lib/split/version.rb +1 -1
- data/lib/split/zscore.rb +1 -1
- data/lib/split.rb +34 -41
- data/spec/algorithms/block_randomization_spec.rb +32 -0
- data/spec/alternative_spec.rb +31 -0
- data/spec/configuration_spec.rb +19 -5
- data/spec/dashboard_helpers_spec.rb +14 -0
- data/spec/dashboard_spec.rb +15 -0
- data/spec/encapsulated_helper_spec.rb +35 -4
- data/spec/experiment_spec.rb +38 -7
- data/spec/helper_spec.rb +59 -16
- data/spec/persistence/dual_adapter_spec.rb +102 -0
- data/spec/redis_interface_spec.rb +111 -0
- data/spec/spec_helper.rb +11 -10
- data/spec/split_spec.rb +43 -0
- data/split.gemspec +2 -3
- metadata +23 -26
- data/gemfiles/4.1.gemfile +0 -8
- data/lib/split/algorithms.rb +0 -4
- data/lib/split/extensions/array.rb +0 -5
- data/lib/split/extensions.rb +0 -4
data/lib/split/experiment.rb
CHANGED
|
@@ -80,29 +80,15 @@ module Split
|
|
|
80
80
|
validate!
|
|
81
81
|
|
|
82
82
|
if new_record?
|
|
83
|
-
Split.redis.sadd(:experiments, name)
|
|
84
83
|
start unless Split.configuration.start_manually
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
save_metadata
|
|
88
|
-
else
|
|
89
|
-
existing_alternatives = load_alternatives_from_redis
|
|
90
|
-
existing_goals = Split::GoalsCollection.new(@name).load_from_redis
|
|
91
|
-
existing_metadata = load_metadata_from_redis
|
|
92
|
-
unless existing_alternatives == @alternatives.map(&:name) && existing_goals == @goals && existing_metadata == @metadata
|
|
93
|
-
reset
|
|
94
|
-
@alternatives.each(&:delete)
|
|
95
|
-
goals_collection.delete
|
|
96
|
-
delete_metadata
|
|
97
|
-
Split.redis.del(@name)
|
|
98
|
-
@alternatives.reverse.each {|a| Split.redis.lpush(name, a.name)}
|
|
99
|
-
goals_collection.save
|
|
100
|
-
save_metadata
|
|
101
|
-
end
|
|
84
|
+
elsif experiment_configuration_has_changed?
|
|
85
|
+
reset unless Split.configuration.reset_manually
|
|
102
86
|
end
|
|
103
87
|
|
|
104
|
-
|
|
105
|
-
|
|
88
|
+
persist_experiment_configuration if new_record? || experiment_configuration_has_changed?
|
|
89
|
+
|
|
90
|
+
redis.hset(experiment_config_key, :resettable, resettable)
|
|
91
|
+
redis.hset(experiment_config_key, :algorithm, algorithm.to_s)
|
|
106
92
|
self
|
|
107
93
|
end
|
|
108
94
|
|
|
@@ -115,7 +101,7 @@ module Split
|
|
|
115
101
|
end
|
|
116
102
|
|
|
117
103
|
def new_record?
|
|
118
|
-
!
|
|
104
|
+
!redis.exists(name)
|
|
119
105
|
end
|
|
120
106
|
|
|
121
107
|
def ==(obj)
|
|
@@ -149,8 +135,9 @@ module Split
|
|
|
149
135
|
end
|
|
150
136
|
|
|
151
137
|
def winner
|
|
152
|
-
|
|
153
|
-
|
|
138
|
+
experiment_winner = redis.hget(:experiment_winner, name)
|
|
139
|
+
if experiment_winner
|
|
140
|
+
Split::Alternative.new(experiment_winner, name)
|
|
154
141
|
else
|
|
155
142
|
nil
|
|
156
143
|
end
|
|
@@ -161,7 +148,7 @@ module Split
|
|
|
161
148
|
end
|
|
162
149
|
|
|
163
150
|
def winner=(winner_name)
|
|
164
|
-
|
|
151
|
+
redis.hset(:experiment_winner, name, winner_name.to_s)
|
|
165
152
|
end
|
|
166
153
|
|
|
167
154
|
def participant_count
|
|
@@ -173,21 +160,21 @@ module Split
|
|
|
173
160
|
end
|
|
174
161
|
|
|
175
162
|
def reset_winner
|
|
176
|
-
|
|
163
|
+
redis.hdel(:experiment_winner, name)
|
|
177
164
|
end
|
|
178
165
|
|
|
179
166
|
def start
|
|
180
|
-
|
|
167
|
+
redis.hset(:experiment_start_times, @name, Time.now.to_i)
|
|
181
168
|
end
|
|
182
169
|
|
|
183
170
|
def start_time
|
|
184
|
-
t =
|
|
171
|
+
t = redis.hget(:experiment_start_times, @name)
|
|
185
172
|
if t
|
|
186
173
|
# Check if stored time is an integer
|
|
187
174
|
if t =~ /^[-+]?[0-9]+$/
|
|
188
|
-
|
|
175
|
+
Time.at(t.to_i)
|
|
189
176
|
else
|
|
190
|
-
|
|
177
|
+
Time.parse(t)
|
|
191
178
|
end
|
|
192
179
|
end
|
|
193
180
|
end
|
|
@@ -205,11 +192,11 @@ module Split
|
|
|
205
192
|
end
|
|
206
193
|
|
|
207
194
|
def version
|
|
208
|
-
@version ||= (
|
|
195
|
+
@version ||= (redis.get("#{name}:version").to_i || 0)
|
|
209
196
|
end
|
|
210
197
|
|
|
211
198
|
def increment_version
|
|
212
|
-
@version =
|
|
199
|
+
@version = redis.incr("#{name}:version")
|
|
213
200
|
end
|
|
214
201
|
|
|
215
202
|
def key
|
|
@@ -247,24 +234,21 @@ module Split
|
|
|
247
234
|
def delete
|
|
248
235
|
Split.configuration.on_before_experiment_delete.call(self)
|
|
249
236
|
if Split.configuration.start_manually
|
|
250
|
-
|
|
237
|
+
redis.hdel(:experiment_start_times, @name)
|
|
251
238
|
end
|
|
252
|
-
alternatives.each(&:delete)
|
|
253
239
|
reset_winner
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
goals_collection.delete
|
|
257
|
-
delete_metadata
|
|
240
|
+
redis.srem(:experiments, name)
|
|
241
|
+
remove_experiment_configuration
|
|
258
242
|
Split.configuration.on_experiment_delete.call(self)
|
|
259
243
|
increment_version
|
|
260
244
|
end
|
|
261
245
|
|
|
262
246
|
def delete_metadata
|
|
263
|
-
|
|
247
|
+
redis.del(metadata_key)
|
|
264
248
|
end
|
|
265
249
|
|
|
266
250
|
def load_from_redis
|
|
267
|
-
exp_config =
|
|
251
|
+
exp_config = redis.hgetall(experiment_config_key)
|
|
268
252
|
|
|
269
253
|
options = {
|
|
270
254
|
resettable: exp_config['resettable'],
|
|
@@ -297,8 +281,6 @@ module Split
|
|
|
297
281
|
end
|
|
298
282
|
|
|
299
283
|
def estimate_winning_alternative(goal = nil)
|
|
300
|
-
# TODO - refactor out functionality to work with and without goals
|
|
301
|
-
|
|
302
284
|
# initialize a hash of beta distributions based on the alternatives' conversion rates
|
|
303
285
|
beta_params = calc_beta_params(goal)
|
|
304
286
|
|
|
@@ -395,11 +377,11 @@ module Split
|
|
|
395
377
|
end
|
|
396
378
|
|
|
397
379
|
def calc_time=(time)
|
|
398
|
-
|
|
380
|
+
redis.hset(experiment_config_key, :calc_time, time)
|
|
399
381
|
end
|
|
400
382
|
|
|
401
383
|
def calc_time
|
|
402
|
-
|
|
384
|
+
redis.hget(experiment_config_key, :calc_time).to_i
|
|
403
385
|
end
|
|
404
386
|
|
|
405
387
|
def jstring(goal = nil)
|
|
@@ -418,11 +400,11 @@ module Split
|
|
|
418
400
|
end
|
|
419
401
|
|
|
420
402
|
def load_metadata_from_configuration
|
|
421
|
-
|
|
403
|
+
Split.configuration.experiment_for(@name)[:metadata]
|
|
422
404
|
end
|
|
423
405
|
|
|
424
406
|
def load_metadata_from_redis
|
|
425
|
-
meta =
|
|
407
|
+
meta = redis.get(metadata_key)
|
|
426
408
|
JSON.parse(meta) unless meta.nil?
|
|
427
409
|
end
|
|
428
410
|
|
|
@@ -437,22 +419,49 @@ module Split
|
|
|
437
419
|
end
|
|
438
420
|
|
|
439
421
|
def load_alternatives_from_redis
|
|
440
|
-
case
|
|
422
|
+
case redis.type(@name)
|
|
441
423
|
when 'set' # convert legacy sets to lists
|
|
442
|
-
alts =
|
|
443
|
-
|
|
444
|
-
alts.reverse.each {|a|
|
|
445
|
-
|
|
424
|
+
alts = redis.smembers(@name)
|
|
425
|
+
redis.del(@name)
|
|
426
|
+
alts.reverse.each {|a| redis.lpush(@name, a) }
|
|
427
|
+
redis.lrange(@name, 0, -1)
|
|
446
428
|
else
|
|
447
|
-
|
|
429
|
+
redis.lrange(@name, 0, -1)
|
|
448
430
|
end
|
|
449
431
|
end
|
|
450
432
|
|
|
451
|
-
|
|
452
|
-
|
|
433
|
+
private
|
|
434
|
+
|
|
435
|
+
def redis
|
|
436
|
+
Split.redis
|
|
453
437
|
end
|
|
454
438
|
|
|
455
|
-
|
|
439
|
+
def redis_interface
|
|
440
|
+
RedisInterface.new
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
def persist_experiment_configuration
|
|
444
|
+
redis_interface.add_to_set(:experiments, name)
|
|
445
|
+
redis_interface.persist_list(name, @alternatives.map(&:name))
|
|
446
|
+
goals_collection.save
|
|
447
|
+
redis.set(metadata_key, @metadata.to_json) unless @metadata.nil?
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def remove_experiment_configuration
|
|
451
|
+
@alternatives.each(&:delete)
|
|
452
|
+
goals_collection.delete
|
|
453
|
+
delete_metadata
|
|
454
|
+
redis.del(@name)
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
def experiment_configuration_has_changed?
|
|
458
|
+
existing_alternatives = load_alternatives_from_redis
|
|
459
|
+
existing_goals = Split::GoalsCollection.new(@name).load_from_redis
|
|
460
|
+
existing_metadata = load_metadata_from_redis
|
|
461
|
+
existing_alternatives != @alternatives.map(&:name) ||
|
|
462
|
+
existing_goals != @goals ||
|
|
463
|
+
existing_metadata != @metadata
|
|
464
|
+
end
|
|
456
465
|
|
|
457
466
|
def goals_collection
|
|
458
467
|
Split::GoalsCollection.new(@name, @goals)
|
data/lib/split/helper.rb
CHANGED
|
@@ -76,6 +76,26 @@ module Split
|
|
|
76
76
|
Split.configuration.db_failover_on_db_error.call(e)
|
|
77
77
|
end
|
|
78
78
|
|
|
79
|
+
def ab_record_extra_info(metric_descriptor, key, value = 1)
|
|
80
|
+
return if exclude_visitor? || Split.configuration.disabled?
|
|
81
|
+
metric_descriptor, goals = normalize_metric(metric_descriptor)
|
|
82
|
+
experiments = Metric.possible_experiments(metric_descriptor)
|
|
83
|
+
|
|
84
|
+
if experiments.any?
|
|
85
|
+
experiments.each do |experiment|
|
|
86
|
+
alternative_name = ab_user[experiment.key]
|
|
87
|
+
|
|
88
|
+
if alternative_name
|
|
89
|
+
alternative = experiment.alternatives.find{|alt| alt.name == alternative_name}
|
|
90
|
+
alternative.record_extra_info(key, value) if alternative
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
rescue => e
|
|
95
|
+
raise unless Split.configuration.db_failover
|
|
96
|
+
Split.configuration.db_failover_on_db_error.call(e)
|
|
97
|
+
end
|
|
98
|
+
|
|
79
99
|
def override_present?(experiment_name)
|
|
80
100
|
override_alternative(experiment_name)
|
|
81
101
|
end
|
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
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module Split
|
|
2
|
+
# Simplifies the interface to Redis.
|
|
3
|
+
class RedisInterface
|
|
4
|
+
def initialize
|
|
5
|
+
self.redis = Split.redis
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def persist_list(list_name, list_values)
|
|
9
|
+
max_index = list_length(list_name) - 1
|
|
10
|
+
list_values.each_with_index do |value, index|
|
|
11
|
+
if index > max_index
|
|
12
|
+
add_to_list(list_name, value)
|
|
13
|
+
else
|
|
14
|
+
set_list_index(list_name, index, value)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
make_list_length(list_name, list_values.length)
|
|
18
|
+
list_values
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def add_to_list(list_name, value)
|
|
22
|
+
redis.rpush(list_name, value)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def set_list_index(list_name, index, value)
|
|
26
|
+
redis.lset(list_name, index, value)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def list_length(list_name)
|
|
30
|
+
redis.llen(list_name)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def remove_last_item_from_list(list_name)
|
|
34
|
+
redis.rpop(list_name)
|
|
35
|
+
end
|
|
36
|
+
|
|
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
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def add_to_set(set_name, value)
|
|
44
|
+
redis.sadd(set_name, value) unless redis.sismember(set_name, value)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
attr_accessor :redis
|
|
50
|
+
end
|
|
51
|
+
end
|
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
|
|
@@ -21,7 +23,7 @@ module Split
|
|
|
21
23
|
def max_experiments_reached?(experiment_key)
|
|
22
24
|
if Split.configuration.allow_multiple_experiments == 'control'
|
|
23
25
|
experiments = active_experiments
|
|
24
|
-
count_control = experiments.
|
|
26
|
+
count_control = experiments.count {|k,v| k == experiment_key || v == 'control'}
|
|
25
27
|
experiments.size > count_control
|
|
26
28
|
else
|
|
27
29
|
!Split.configuration.allow_multiple_experiments &&
|
data/lib/split/version.rb
CHANGED
data/lib/split/zscore.rb
CHANGED
data/lib/split.rb
CHANGED
|
@@ -1,54 +1,47 @@
|
|
|
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
|
-
trial
|
|
15
|
-
user
|
|
16
|
-
version
|
|
17
|
-
zscore].each do |f|
|
|
18
|
-
require "split/#{f}"
|
|
19
|
-
end
|
|
2
|
+
require 'redis'
|
|
20
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/metric'
|
|
17
|
+
require 'split/persistence'
|
|
18
|
+
require 'split/redis_interface'
|
|
19
|
+
require 'split/trial'
|
|
20
|
+
require 'split/user'
|
|
21
|
+
require 'split/version'
|
|
22
|
+
require 'split/zscore'
|
|
21
23
|
require 'split/engine' if defined?(Rails)
|
|
22
|
-
require 'redis/namespace'
|
|
23
24
|
|
|
24
25
|
module Split
|
|
25
26
|
extend self
|
|
26
27
|
attr_accessor :configuration
|
|
27
28
|
|
|
28
29
|
# Accepts:
|
|
29
|
-
# 1. A
|
|
30
|
-
# 2.
|
|
31
|
-
# 3.
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
# or `Redis::Namespace`.
|
|
30
|
+
# 1. A redis URL (valid for `Redis.new(url: url)`)
|
|
31
|
+
# 2. an options hash compatible with `Redis.new`
|
|
32
|
+
# 3. or a valid Redis instance (one that responds to `#smembers`). Likely,
|
|
33
|
+
# this will be an instance of either `Redis`, `Redis::Client`,
|
|
34
|
+
# `Redis::DistRedis`, or `Redis::Namespace`.
|
|
35
35
|
def redis=(server)
|
|
36
|
-
if server.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
redis = Redis.new(:host => host, :port => port,
|
|
43
|
-
:thread_safe => true, :db => db)
|
|
44
|
-
end
|
|
45
|
-
namespace ||= :split
|
|
46
|
-
|
|
47
|
-
@redis = Redis::Namespace.new(namespace, :redis => redis)
|
|
48
|
-
elsif server.respond_to? :namespace=
|
|
49
|
-
@redis = server
|
|
36
|
+
@redis = if server.is_a?(String)
|
|
37
|
+
Redis.new(:url => server, :thread_safe => true)
|
|
38
|
+
elsif server.is_a?(Hash)
|
|
39
|
+
Redis.new(server.merge(:thread_safe => true))
|
|
40
|
+
elsif server.respond_to?(:smembers)
|
|
41
|
+
server
|
|
50
42
|
else
|
|
51
|
-
|
|
43
|
+
raise ArgumentError,
|
|
44
|
+
"You must supply a url, options hash or valid Redis connection instance"
|
|
52
45
|
end
|
|
53
46
|
end
|
|
54
47
|
|
|
@@ -56,7 +49,7 @@ module Split
|
|
|
56
49
|
# create a new one.
|
|
57
50
|
def redis
|
|
58
51
|
return @redis if @redis
|
|
59
|
-
self.redis = self.configuration.
|
|
52
|
+
self.redis = self.configuration.redis
|
|
60
53
|
self.redis
|
|
61
54
|
end
|
|
62
55
|
|
|
@@ -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
|
data/spec/configuration_spec.rb
CHANGED
|
@@ -212,20 +212,34 @@ describe Split::Configuration do
|
|
|
212
212
|
expect(@config.normalized_experiments).to eq({:my_experiment=>{:alternatives=>[{"control_opt"=>0.67}, [{"second_opt"=>0.1}, {"third_opt"=>0.23}]]}})
|
|
213
213
|
end
|
|
214
214
|
|
|
215
|
-
context
|
|
215
|
+
context 'redis_url configuration [DEPRECATED]' do
|
|
216
|
+
it 'should warn on set and assign to #redis' do
|
|
217
|
+
expect(@config).to receive(:warn).with(/\[DEPRECATED\]/) { nil }
|
|
218
|
+
@config.redis_url = 'example_url'
|
|
219
|
+
expect(@config.redis).to eq('example_url')
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
it 'should warn on get and return #redis' do
|
|
223
|
+
expect(@config).to receive(:warn).with(/\[DEPRECATED\]/) { nil }
|
|
224
|
+
@config.redis = 'example_url'
|
|
225
|
+
expect(@config.redis_url).to eq('example_url')
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
context "redis configuration" do
|
|
216
230
|
it "should default to local redis server" do
|
|
217
|
-
expect(@config.
|
|
231
|
+
expect(@config.redis).to eq("redis://localhost:6379")
|
|
218
232
|
end
|
|
219
233
|
|
|
220
234
|
it "should allow for redis url to be configured" do
|
|
221
|
-
@config.
|
|
222
|
-
expect(@config.
|
|
235
|
+
@config.redis = "custom_redis_url"
|
|
236
|
+
expect(@config.redis).to eq("custom_redis_url")
|
|
223
237
|
end
|
|
224
238
|
|
|
225
239
|
context "provided REDIS_URL environment variable" do
|
|
226
240
|
it "should use the ENV variable" do
|
|
227
241
|
ENV['REDIS_URL'] = "env_redis_url"
|
|
228
|
-
expect(Split::Configuration.new.
|
|
242
|
+
expect(Split::Configuration.new.redis).to eq("env_redis_url")
|
|
229
243
|
end
|
|
230
244
|
end
|
|
231
245
|
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/dashboard_spec.rb
CHANGED
|
@@ -73,6 +73,21 @@ describe Split::Dashboard do
|
|
|
73
73
|
end
|
|
74
74
|
end
|
|
75
75
|
|
|
76
|
+
describe "force alternative" do
|
|
77
|
+
let!(:user) do
|
|
78
|
+
Split::User.new(@app, { experiment.name => 'a' })
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
before do
|
|
82
|
+
allow(Split::User).to receive(:new).and_return(user)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it "should set current user's alternative" do
|
|
86
|
+
post "/force_alternative?experiment=#{experiment.name}", alternative: "b"
|
|
87
|
+
expect(user[experiment.name]).to eq("b")
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
76
91
|
describe "index page" do
|
|
77
92
|
context "with winner" do
|
|
78
93
|
before { experiment.winner = 'red' }
|
|
@@ -4,18 +4,49 @@ require 'spec_helper'
|
|
|
4
4
|
describe Split::EncapsulatedHelper do
|
|
5
5
|
include Split::EncapsulatedHelper
|
|
6
6
|
|
|
7
|
-
before do
|
|
8
|
-
allow_any_instance_of(Split::EncapsulatedHelper::ContextShim).to receive(:ab_user)
|
|
9
|
-
.and_return(mock_user)
|
|
10
|
-
end
|
|
11
7
|
|
|
12
8
|
def params
|
|
13
9
|
raise NoMethodError, 'This method is not really defined'
|
|
14
10
|
end
|
|
15
11
|
|
|
16
12
|
describe "ab_test" do
|
|
13
|
+
before do
|
|
14
|
+
allow_any_instance_of(Split::EncapsulatedHelper::ContextShim).to receive(:ab_user)
|
|
15
|
+
.and_return(mock_user)
|
|
16
|
+
end
|
|
17
|
+
|
|
17
18
|
it "should not raise an error when params raises an error" do
|
|
19
|
+
expect{ params }.to raise_error(NoMethodError)
|
|
18
20
|
expect(lambda { ab_test('link_color', 'blue', 'red') }).not_to raise_error
|
|
19
21
|
end
|
|
22
|
+
|
|
23
|
+
it "calls the block with selected alternative" do
|
|
24
|
+
expect{|block| ab_test('link_color', 'red', 'red', &block) }.to yield_with_args('red', nil)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
context "inside a view" do
|
|
28
|
+
|
|
29
|
+
it "works inside ERB" do
|
|
30
|
+
require 'erb'
|
|
31
|
+
template = ERB.new(<<-ERB.split(/\s+/s).map(&:strip).join(' '), nil, "%")
|
|
32
|
+
foo <% ab_test(:foo, '1', '2') do |alt, meta| %>
|
|
33
|
+
static <%= alt %>
|
|
34
|
+
<% end %>
|
|
35
|
+
ERB
|
|
36
|
+
expect(template.result(binding)).to match /foo static \d/
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
describe "context" do
|
|
43
|
+
it 'is passed in shim' do
|
|
44
|
+
ctx = Class.new{
|
|
45
|
+
include Split::EncapsulatedHelper
|
|
46
|
+
public :session
|
|
47
|
+
}.new
|
|
48
|
+
expect(ctx).to receive(:session){{}}
|
|
49
|
+
expect{ ctx.ab_test('link_color', 'blue', 'red') }.not_to raise_error
|
|
50
|
+
end
|
|
20
51
|
end
|
|
21
52
|
end
|