split 3.0.0 → 4.0.1
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 +5 -5
- data/.eslintrc +1 -1
- data/.github/FUNDING.yml +1 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +24 -0
- data/.github/dependabot.yml +7 -0
- data/.github/workflows/ci.yml +71 -0
- data/.rspec +1 -0
- data/.rubocop.yml +71 -1044
- data/.rubocop_todo.yml +226 -0
- data/Appraisals +12 -1
- data/CHANGELOG.md +157 -0
- data/CODE_OF_CONDUCT.md +3 -3
- data/CONTRIBUTING.md +54 -5
- data/Gemfile +2 -0
- data/LICENSE +1 -1
- data/README.md +232 -121
- data/Rakefile +2 -0
- data/gemfiles/5.0.gemfile +1 -2
- data/gemfiles/{4.2.gemfile → 5.1.gemfile} +2 -2
- data/gemfiles/5.2.gemfile +9 -0
- data/gemfiles/6.0.gemfile +9 -0
- data/gemfiles/6.1.gemfile +9 -0
- data/gemfiles/7.0.gemfile +9 -0
- data/lib/split/algorithms/block_randomization.rb +2 -0
- data/lib/split/algorithms/weighted_sample.rb +2 -1
- data/lib/split/algorithms/whiplash.rb +3 -2
- data/lib/split/alternative.rb +7 -3
- data/lib/split/cache.rb +28 -0
- data/lib/split/combined_experiments_helper.rb +38 -0
- data/lib/split/configuration.rb +24 -13
- data/lib/split/dashboard/helpers.rb +3 -2
- data/lib/split/dashboard/pagination_helpers.rb +87 -0
- data/lib/split/dashboard/paginator.rb +17 -0
- data/lib/split/dashboard/public/dashboard.js +10 -0
- data/lib/split/dashboard/public/style.css +14 -0
- data/lib/split/dashboard/views/_controls.erb +13 -0
- data/lib/split/dashboard/views/index.erb +5 -1
- data/lib/split/dashboard/views/layout.erb +1 -1
- data/lib/split/dashboard.rb +21 -1
- data/lib/split/encapsulated_helper.rb +3 -2
- data/lib/split/engine.rb +7 -2
- data/lib/split/exceptions.rb +1 -0
- data/lib/split/experiment.rb +103 -69
- data/lib/split/experiment_catalog.rb +1 -3
- data/lib/split/extensions/string.rb +1 -0
- data/lib/split/goals_collection.rb +2 -0
- data/lib/split/helper.rb +42 -9
- data/lib/split/metric.rb +2 -1
- data/lib/split/persistence/cookie_adapter.rb +58 -15
- data/lib/split/persistence/dual_adapter.rb +54 -12
- data/lib/split/persistence/redis_adapter.rb +5 -0
- data/lib/split/persistence/session_adapter.rb +1 -0
- data/lib/split/persistence.rb +4 -2
- data/lib/split/redis_interface.rb +9 -30
- data/lib/split/trial.rb +25 -17
- data/lib/split/user.rb +20 -4
- data/lib/split/version.rb +2 -4
- data/lib/split/zscore.rb +1 -0
- data/lib/split.rb +17 -3
- data/spec/alternative_spec.rb +13 -1
- data/spec/cache_spec.rb +88 -0
- data/spec/combined_experiments_helper_spec.rb +57 -0
- data/spec/configuration_spec.rb +17 -15
- data/spec/dashboard/pagination_helpers_spec.rb +200 -0
- data/spec/dashboard/paginator_spec.rb +37 -0
- data/spec/dashboard_helpers_spec.rb +2 -2
- data/spec/dashboard_spec.rb +78 -17
- data/spec/encapsulated_helper_spec.rb +2 -2
- data/spec/experiment_spec.rb +117 -13
- data/spec/goals_collection_spec.rb +1 -1
- data/spec/helper_spec.rb +211 -112
- data/spec/persistence/cookie_adapter_spec.rb +90 -23
- data/spec/persistence/dual_adapter_spec.rb +160 -68
- data/spec/persistence/redis_adapter_spec.rb +9 -0
- data/spec/redis_interface_spec.rb +0 -69
- data/spec/spec_helper.rb +5 -6
- data/spec/split_spec.rb +7 -7
- data/spec/trial_spec.rb +65 -19
- data/spec/user_spec.rb +45 -3
- data/split.gemspec +20 -10
- metadata +61 -35
- data/.travis.yml +0 -16
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'forwardable'
|
|
4
|
-
|
|
5
3
|
module Split
|
|
6
4
|
module Persistence
|
|
7
5
|
class DualAdapter
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
def self.with_config(options={})
|
|
7
|
+
self.config.merge!(options)
|
|
8
|
+
self
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.config
|
|
12
|
+
@config ||= {}
|
|
13
|
+
end
|
|
10
14
|
|
|
11
15
|
def initialize(context)
|
|
12
16
|
if logged_in = self.class.config[:logged_in]
|
|
@@ -22,22 +26,60 @@ module Split
|
|
|
22
26
|
raise "Please configure :logged_out_adapter"
|
|
23
27
|
end
|
|
24
28
|
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
@fallback_to_logged_out_adapter =
|
|
30
|
+
self.class.config[:fallback_to_logged_out_adapter] || false
|
|
31
|
+
@logged_in = logged_in.call(context)
|
|
32
|
+
@logged_in_adapter = logged_in_adapter.new(context)
|
|
33
|
+
@logged_out_adapter = logged_out_adapter.new(context)
|
|
34
|
+
@active_adapter = @logged_in ? @logged_in_adapter : @logged_out_adapter
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def keys
|
|
38
|
+
if @fallback_to_logged_out_adapter
|
|
39
|
+
(@logged_in_adapter.keys + @logged_out_adapter.keys).uniq
|
|
27
40
|
else
|
|
28
|
-
@
|
|
41
|
+
@active_adapter.keys
|
|
29
42
|
end
|
|
30
43
|
end
|
|
31
44
|
|
|
32
|
-
def
|
|
33
|
-
|
|
34
|
-
|
|
45
|
+
def [](key)
|
|
46
|
+
if @fallback_to_logged_out_adapter
|
|
47
|
+
@logged_in && @logged_in_adapter[key] || @logged_out_adapter[key]
|
|
48
|
+
else
|
|
49
|
+
@active_adapter[key]
|
|
50
|
+
end
|
|
35
51
|
end
|
|
36
52
|
|
|
37
|
-
def
|
|
38
|
-
@
|
|
53
|
+
def []=(key, value)
|
|
54
|
+
if @fallback_to_logged_out_adapter
|
|
55
|
+
@logged_in_adapter[key] = value if @logged_in
|
|
56
|
+
old_value = @logged_out_adapter[key]
|
|
57
|
+
@logged_out_adapter[key] = value
|
|
58
|
+
|
|
59
|
+
decrement_participation(key, old_value) if decrement_participation?(old_value, value)
|
|
60
|
+
else
|
|
61
|
+
@active_adapter[key] = value
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def delete(key)
|
|
66
|
+
if @fallback_to_logged_out_adapter
|
|
67
|
+
@logged_in_adapter.delete(key)
|
|
68
|
+
@logged_out_adapter.delete(key)
|
|
69
|
+
else
|
|
70
|
+
@active_adapter.delete(key)
|
|
71
|
+
end
|
|
39
72
|
end
|
|
40
73
|
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def decrement_participation?(old_value, value)
|
|
77
|
+
!old_value.nil? && !value.nil? && old_value != value
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def decrement_participation(key, value)
|
|
81
|
+
Split.redis.hincrby("#{key}:#{value}", 'participant_count', -1)
|
|
82
|
+
end
|
|
41
83
|
end
|
|
42
84
|
end
|
|
43
85
|
end
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module Split
|
|
3
4
|
module Persistence
|
|
4
5
|
class RedisAdapter
|
|
@@ -39,6 +40,10 @@ module Split
|
|
|
39
40
|
Split.redis.hkeys(redis_key)
|
|
40
41
|
end
|
|
41
42
|
|
|
43
|
+
def self.find(user_id)
|
|
44
|
+
new(nil, user_id)
|
|
45
|
+
end
|
|
46
|
+
|
|
42
47
|
def self.with_config(options={})
|
|
43
48
|
self.config.merge!(options)
|
|
44
49
|
self
|
data/lib/split/persistence.rb
CHANGED
|
@@ -8,8 +8,10 @@ module Split
|
|
|
8
8
|
require 'split/persistence/session_adapter'
|
|
9
9
|
|
|
10
10
|
ADAPTERS = {
|
|
11
|
-
:
|
|
12
|
-
:
|
|
11
|
+
cookie: Split::Persistence::CookieAdapter,
|
|
12
|
+
session: Split::Persistence::SessionAdapter,
|
|
13
|
+
redis: Split::Persistence::RedisAdapter,
|
|
14
|
+
dual_adapter: Split::Persistence::DualAdapter
|
|
13
15
|
}.freeze
|
|
14
16
|
|
|
15
17
|
def self.adapter
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Split
|
|
2
4
|
# Simplifies the interface to Redis.
|
|
3
5
|
class RedisInterface
|
|
@@ -6,42 +8,19 @@ module Split
|
|
|
6
8
|
end
|
|
7
9
|
|
|
8
10
|
def persist_list(list_name, list_values)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
set_list_index(list_name, index, value)
|
|
11
|
+
if list_values.length > 0
|
|
12
|
+
redis.multi do |multi|
|
|
13
|
+
tmp_list = "#{list_name}_tmp"
|
|
14
|
+
multi.rpush(tmp_list, list_values)
|
|
15
|
+
multi.rename(tmp_list, list_name)
|
|
15
16
|
end
|
|
16
17
|
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
18
|
|
|
29
|
-
|
|
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
|
|
19
|
+
list_values
|
|
41
20
|
end
|
|
42
21
|
|
|
43
22
|
def add_to_set(set_name, value)
|
|
44
|
-
redis.sadd(set_name, value)
|
|
23
|
+
redis.sadd(set_name, value)
|
|
45
24
|
end
|
|
46
25
|
|
|
47
26
|
private
|
data/lib/split/trial.rb
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module Split
|
|
3
4
|
class Trial
|
|
5
|
+
attr_accessor :goals
|
|
4
6
|
attr_accessor :experiment
|
|
5
|
-
|
|
7
|
+
attr_writer :metadata
|
|
6
8
|
|
|
7
9
|
def initialize(attrs = {})
|
|
8
10
|
self.experiment = attrs.delete(:experiment)
|
|
9
11
|
self.alternative = attrs.delete(:alternative)
|
|
10
12
|
self.metadata = attrs.delete(:metadata)
|
|
13
|
+
self.goals = attrs.delete(:goals) || []
|
|
11
14
|
|
|
12
15
|
@user = attrs.delete(:user)
|
|
13
16
|
@options = attrs
|
|
14
17
|
|
|
15
|
-
@
|
|
18
|
+
@alternative_chosen = false
|
|
16
19
|
end
|
|
17
20
|
|
|
18
21
|
def metadata
|
|
@@ -33,7 +36,7 @@ module Split
|
|
|
33
36
|
end
|
|
34
37
|
end
|
|
35
38
|
|
|
36
|
-
def complete!(
|
|
39
|
+
def complete!(context = nil)
|
|
37
40
|
if alternative
|
|
38
41
|
if Array(goals).empty?
|
|
39
42
|
alternative.increment_completion
|
|
@@ -51,8 +54,9 @@ module Split
|
|
|
51
54
|
def choose!(context = nil)
|
|
52
55
|
@user.cleanup_old_experiments!
|
|
53
56
|
# Only run the process once
|
|
54
|
-
return alternative if @
|
|
57
|
+
return alternative if @alternative_chosen
|
|
55
58
|
|
|
59
|
+
new_participant = @user[@experiment.key].nil?
|
|
56
60
|
if override_is_alternative?
|
|
57
61
|
self.alternative = @options[:override]
|
|
58
62
|
if should_store_alternative? && !@user[@experiment.key]
|
|
@@ -68,23 +72,27 @@ module Split
|
|
|
68
72
|
if exclude_user?
|
|
69
73
|
self.alternative = @experiment.control
|
|
70
74
|
else
|
|
71
|
-
|
|
72
|
-
if
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
75
|
+
self.alternative = @user[@experiment.key]
|
|
76
|
+
if alternative.nil?
|
|
77
|
+
if @experiment.cohorting_disabled?
|
|
78
|
+
self.alternative = @experiment.control
|
|
79
|
+
else
|
|
80
|
+
self.alternative = @experiment.next_alternative
|
|
81
|
+
|
|
82
|
+
# Increment the number of participants since we are actually choosing a new alternative
|
|
83
|
+
self.alternative.increment_participation
|
|
84
|
+
|
|
85
|
+
run_callback context, Split.configuration.on_trial_choose
|
|
86
|
+
end
|
|
81
87
|
end
|
|
82
88
|
end
|
|
83
89
|
end
|
|
84
90
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
91
|
+
new_participant_and_cohorting_disabled = new_participant && @experiment.cohorting_disabled?
|
|
92
|
+
|
|
93
|
+
@user[@experiment.key] = alternative.name unless @experiment.has_winner? || !should_store_alternative? || new_participant_and_cohorting_disabled
|
|
94
|
+
@alternative_chosen = true
|
|
95
|
+
run_callback context, Split.configuration.on_trial unless @options[:disabled] || Split.configuration.disabled? || new_participant_and_cohorting_disabled
|
|
88
96
|
alternative
|
|
89
97
|
end
|
|
90
98
|
|
data/lib/split/user.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'forwardable'
|
|
2
4
|
|
|
3
5
|
module Split
|
|
@@ -6,11 +8,13 @@ module Split
|
|
|
6
8
|
def_delegators :@user, :keys, :[], :[]=, :delete
|
|
7
9
|
attr_reader :user
|
|
8
10
|
|
|
9
|
-
def initialize(context, adapter=nil)
|
|
11
|
+
def initialize(context, adapter = nil)
|
|
10
12
|
@user = adapter || Split::Persistence.adapter.new(context)
|
|
13
|
+
@cleaned_up = false
|
|
11
14
|
end
|
|
12
15
|
|
|
13
16
|
def cleanup_old_experiments!
|
|
17
|
+
return if @cleaned_up
|
|
14
18
|
keys_without_finished(user.keys).each do |key|
|
|
15
19
|
experiment = ExperimentCatalog.find key_without_version(key)
|
|
16
20
|
if experiment.nil? || experiment.has_winner? || experiment.start_time.nil?
|
|
@@ -18,12 +22,14 @@ module Split
|
|
|
18
22
|
user.delete Experiment.finished_key(key)
|
|
19
23
|
end
|
|
20
24
|
end
|
|
25
|
+
@cleaned_up = true
|
|
21
26
|
end
|
|
22
27
|
|
|
23
28
|
def max_experiments_reached?(experiment_key)
|
|
24
29
|
if Split.configuration.allow_multiple_experiments == 'control'
|
|
25
30
|
experiments = active_experiments
|
|
26
|
-
|
|
31
|
+
experiment_key_without_version = key_without_version(experiment_key)
|
|
32
|
+
count_control = experiments.count {|k, v| k == experiment_key_without_version || v == 'control'}
|
|
27
33
|
experiments.size > count_control
|
|
28
34
|
else
|
|
29
35
|
!Split.configuration.allow_multiple_experiments &&
|
|
@@ -32,13 +38,13 @@ module Split
|
|
|
32
38
|
end
|
|
33
39
|
|
|
34
40
|
def cleanup_old_versions!(experiment)
|
|
35
|
-
keys = user.keys.select { |k| k
|
|
41
|
+
keys = user.keys.select { |k| key_without_version(k) == experiment.name }
|
|
36
42
|
keys_without_experiment(keys, experiment.key).each { |key| user.delete(key) }
|
|
37
43
|
end
|
|
38
44
|
|
|
39
45
|
def active_experiments
|
|
40
46
|
experiment_pairs = {}
|
|
41
|
-
user.keys.each do |key|
|
|
47
|
+
keys_without_finished(user.keys).each do |key|
|
|
42
48
|
Metric.possible_experiments(key_without_version(key)).each do |experiment|
|
|
43
49
|
if !experiment.has_winner?
|
|
44
50
|
experiment_pairs[key_without_version(key)] = user[key]
|
|
@@ -48,6 +54,16 @@ module Split
|
|
|
48
54
|
experiment_pairs
|
|
49
55
|
end
|
|
50
56
|
|
|
57
|
+
def self.find(user_id, adapter)
|
|
58
|
+
adapter = adapter.is_a?(Symbol) ? Split::Persistence::ADAPTERS[adapter] : adapter
|
|
59
|
+
|
|
60
|
+
if adapter.respond_to?(:find)
|
|
61
|
+
User.new(nil, adapter.find(user_id))
|
|
62
|
+
else
|
|
63
|
+
nil
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
51
67
|
private
|
|
52
68
|
|
|
53
69
|
def keys_without_experiment(keys, experiment_key)
|
data/lib/split/version.rb
CHANGED
data/lib/split/zscore.rb
CHANGED
data/lib/split.rb
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
require 'redis'
|
|
3
4
|
|
|
4
5
|
require 'split/algorithms/block_randomization'
|
|
5
6
|
require 'split/algorithms/weighted_sample'
|
|
6
7
|
require 'split/algorithms/whiplash'
|
|
7
8
|
require 'split/alternative'
|
|
9
|
+
require 'split/cache'
|
|
8
10
|
require 'split/configuration'
|
|
9
11
|
require 'split/encapsulated_helper'
|
|
10
12
|
require 'split/exceptions'
|
|
@@ -13,6 +15,7 @@ require 'split/experiment_catalog'
|
|
|
13
15
|
require 'split/extensions/string'
|
|
14
16
|
require 'split/goals_collection'
|
|
15
17
|
require 'split/helper'
|
|
18
|
+
require 'split/combined_experiments_helper'
|
|
16
19
|
require 'split/metric'
|
|
17
20
|
require 'split/persistence'
|
|
18
21
|
require 'split/redis_interface'
|
|
@@ -34,9 +37,9 @@ module Split
|
|
|
34
37
|
# `Redis::DistRedis`, or `Redis::Namespace`.
|
|
35
38
|
def redis=(server)
|
|
36
39
|
@redis = if server.is_a?(String)
|
|
37
|
-
Redis.new(:
|
|
40
|
+
Redis.new(url: server)
|
|
38
41
|
elsif server.is_a?(Hash)
|
|
39
|
-
Redis.new(server
|
|
42
|
+
Redis.new(server)
|
|
40
43
|
elsif server.respond_to?(:smembers)
|
|
41
44
|
server
|
|
42
45
|
else
|
|
@@ -63,6 +66,17 @@ module Split
|
|
|
63
66
|
self.configuration ||= Configuration.new
|
|
64
67
|
yield(configuration)
|
|
65
68
|
end
|
|
69
|
+
|
|
70
|
+
def cache(namespace, key, &block)
|
|
71
|
+
Split::Cache.fetch(namespace, key, &block)
|
|
72
|
+
end
|
|
66
73
|
end
|
|
67
74
|
|
|
68
|
-
|
|
75
|
+
# Check to see if being run in a Rails application. If so, wait until before_initialize to run configuration so Gems that create ENV variables have the chance to initialize first.
|
|
76
|
+
if defined?(::Rails)
|
|
77
|
+
class Split::Railtie < Rails::Railtie
|
|
78
|
+
config.before_initialize { Split.configure {} }
|
|
79
|
+
end
|
|
80
|
+
else
|
|
81
|
+
Split.configure {}
|
|
82
|
+
end
|
data/spec/alternative_spec.rb
CHANGED
|
@@ -126,7 +126,7 @@ describe Split::Alternative do
|
|
|
126
126
|
|
|
127
127
|
it "should save to redis" do
|
|
128
128
|
alternative.save
|
|
129
|
-
expect(Split.redis.exists('basket_text:Basket')).to be true
|
|
129
|
+
expect(Split.redis.exists?('basket_text:Basket')).to be true
|
|
130
130
|
end
|
|
131
131
|
|
|
132
132
|
it "should increment participation count" do
|
|
@@ -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
|
data/spec/cache_spec.rb
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'spec_helper'
|
|
3
|
+
|
|
4
|
+
describe Split::Cache do
|
|
5
|
+
|
|
6
|
+
let(:namespace) { :test_namespace }
|
|
7
|
+
let(:key) { :test_key }
|
|
8
|
+
let(:now) { 1606189017 }
|
|
9
|
+
|
|
10
|
+
before { allow(Time).to receive(:now).and_return(now) }
|
|
11
|
+
|
|
12
|
+
describe 'clear' do
|
|
13
|
+
|
|
14
|
+
before { Split.configuration.cache = true }
|
|
15
|
+
|
|
16
|
+
it 'clears the cache' do
|
|
17
|
+
expect(Time).to receive(:now).and_return(now).exactly(2).times
|
|
18
|
+
Split::Cache.fetch(namespace, key) { Time.now }
|
|
19
|
+
Split::Cache.clear
|
|
20
|
+
Split::Cache.fetch(namespace, key) { Time.now }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe 'clear_key' do
|
|
25
|
+
before { Split.configuration.cache = true }
|
|
26
|
+
|
|
27
|
+
it 'clears the cache' do
|
|
28
|
+
expect(Time).to receive(:now).and_return(now).exactly(3).times
|
|
29
|
+
Split::Cache.fetch(namespace, :key1) { Time.now }
|
|
30
|
+
Split::Cache.fetch(namespace, :key2) { Time.now }
|
|
31
|
+
Split::Cache.clear_key(:key1)
|
|
32
|
+
|
|
33
|
+
Split::Cache.fetch(namespace, :key1) { Time.now }
|
|
34
|
+
Split::Cache.fetch(namespace, :key2) { Time.now }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
describe 'fetch' do
|
|
39
|
+
|
|
40
|
+
subject { Split::Cache.fetch(namespace, key) { Time.now } }
|
|
41
|
+
|
|
42
|
+
context 'when cache disabled' do
|
|
43
|
+
|
|
44
|
+
before { Split.configuration.cache = false }
|
|
45
|
+
|
|
46
|
+
it 'returns the yield' do
|
|
47
|
+
expect(subject).to eql(now)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it 'yields every time' do
|
|
51
|
+
expect(Time).to receive(:now).and_return(now).exactly(2).times
|
|
52
|
+
Split::Cache.fetch(namespace, key) { Time.now }
|
|
53
|
+
Split::Cache.fetch(namespace, key) { Time.now }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
context 'when cache enabled' do
|
|
58
|
+
|
|
59
|
+
before { Split.configuration.cache = true }
|
|
60
|
+
|
|
61
|
+
it 'returns the yield' do
|
|
62
|
+
expect(subject).to eql(now)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it 'yields once' do
|
|
66
|
+
expect(Time).to receive(:now).and_return(now).once
|
|
67
|
+
Split::Cache.fetch(namespace, key) { Time.now }
|
|
68
|
+
Split::Cache.fetch(namespace, key) { Time.now }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it 'honors namespace' do
|
|
72
|
+
expect(Split::Cache.fetch(:a, key) { :a }).to eql(:a)
|
|
73
|
+
expect(Split::Cache.fetch(:b, key) { :b }).to eql(:b)
|
|
74
|
+
|
|
75
|
+
expect(Split::Cache.fetch(:a, key) { :a }).to eql(:a)
|
|
76
|
+
expect(Split::Cache.fetch(:b, key) { :b }).to eql(:b)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it 'honors key' do
|
|
80
|
+
expect(Split::Cache.fetch(namespace, :a) { :a }).to eql(:a)
|
|
81
|
+
expect(Split::Cache.fetch(namespace, :b) { :b }).to eql(:b)
|
|
82
|
+
|
|
83
|
+
expect(Split::Cache.fetch(namespace, :a) { :a }).to eql(:a)
|
|
84
|
+
expect(Split::Cache.fetch(namespace, :b) { :b }).to eql(:b)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
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 alternative for all sub experiments and returns the alternative" 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, [{"control" => 0, "test-alt" => 1}])
|
|
53
|
+
|
|
54
|
+
expect(ab_combined_test('combined_exp_1')).to eq('test-alt')
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
data/spec/configuration_spec.rb
CHANGED
|
@@ -212,23 +212,12 @@ 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 '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
215
|
context "redis configuration" do
|
|
230
216
|
it "should default to local redis server" do
|
|
231
|
-
|
|
217
|
+
old_redis_url = ENV['REDIS_URL']
|
|
218
|
+
ENV.delete('REDIS_URL')
|
|
219
|
+
expect(Split::Configuration.new.redis).to eq("redis://localhost:6379")
|
|
220
|
+
ENV['REDIS_URL'] = old_redis_url
|
|
232
221
|
end
|
|
233
222
|
|
|
234
223
|
it "should allow for redis url to be configured" do
|
|
@@ -238,8 +227,10 @@ describe Split::Configuration do
|
|
|
238
227
|
|
|
239
228
|
context "provided REDIS_URL environment variable" do
|
|
240
229
|
it "should use the ENV variable" do
|
|
230
|
+
old_redis_url = ENV['REDIS_URL']
|
|
241
231
|
ENV['REDIS_URL'] = "env_redis_url"
|
|
242
232
|
expect(Split::Configuration.new.redis).to eq("env_redis_url")
|
|
233
|
+
ENV['REDIS_URL'] = old_redis_url
|
|
243
234
|
end
|
|
244
235
|
end
|
|
245
236
|
end
|
|
@@ -255,4 +246,15 @@ describe Split::Configuration do
|
|
|
255
246
|
end
|
|
256
247
|
end
|
|
257
248
|
|
|
249
|
+
context "persistence cookie domain" do
|
|
250
|
+
it "should default to nil" do
|
|
251
|
+
expect(@config.persistence_cookie_domain).to eq(nil)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
it "should allow the persistence cookie domain to be configured" do
|
|
255
|
+
@config.persistence_cookie_domain = '.acme.com'
|
|
256
|
+
expect(@config.persistence_cookie_domain).to eq('.acme.com')
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
258
260
|
end
|