ab-split 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.codeclimate.yml +30 -0
- data/.csslintrc +2 -0
- data/.eslintignore +1 -0
- data/.eslintrc +213 -0
- data/.github/FUNDING.yml +1 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +24 -0
- data/.rspec +1 -0
- data/.rubocop.yml +7 -0
- data/.rubocop_todo.yml +679 -0
- data/.travis.yml +60 -0
- data/Appraisals +19 -0
- data/CHANGELOG.md +696 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/CONTRIBUTING.md +62 -0
- data/Gemfile +7 -0
- data/LICENSE +22 -0
- data/README.md +955 -0
- data/Rakefile +9 -0
- data/ab-split.gemspec +44 -0
- data/gemfiles/4.2.gemfile +9 -0
- data/gemfiles/5.0.gemfile +9 -0
- data/gemfiles/5.1.gemfile +9 -0
- data/gemfiles/5.2.gemfile +9 -0
- data/gemfiles/6.0.gemfile +9 -0
- data/lib/split.rb +76 -0
- data/lib/split/algorithms/block_randomization.rb +23 -0
- data/lib/split/algorithms/weighted_sample.rb +18 -0
- data/lib/split/algorithms/whiplash.rb +38 -0
- data/lib/split/alternative.rb +191 -0
- data/lib/split/combined_experiments_helper.rb +37 -0
- data/lib/split/configuration.rb +255 -0
- data/lib/split/dashboard.rb +74 -0
- data/lib/split/dashboard/helpers.rb +45 -0
- data/lib/split/dashboard/pagination_helpers.rb +86 -0
- data/lib/split/dashboard/paginator.rb +16 -0
- data/lib/split/dashboard/public/dashboard-filtering.js +43 -0
- data/lib/split/dashboard/public/dashboard.js +24 -0
- data/lib/split/dashboard/public/jquery-1.11.1.min.js +4 -0
- data/lib/split/dashboard/public/reset.css +48 -0
- data/lib/split/dashboard/public/style.css +328 -0
- data/lib/split/dashboard/views/_controls.erb +18 -0
- data/lib/split/dashboard/views/_experiment.erb +155 -0
- data/lib/split/dashboard/views/_experiment_with_goal_header.erb +8 -0
- data/lib/split/dashboard/views/index.erb +26 -0
- data/lib/split/dashboard/views/layout.erb +27 -0
- data/lib/split/encapsulated_helper.rb +42 -0
- data/lib/split/engine.rb +15 -0
- data/lib/split/exceptions.rb +6 -0
- data/lib/split/experiment.rb +486 -0
- data/lib/split/experiment_catalog.rb +51 -0
- data/lib/split/extensions/string.rb +16 -0
- data/lib/split/goals_collection.rb +45 -0
- data/lib/split/helper.rb +165 -0
- data/lib/split/metric.rb +101 -0
- data/lib/split/persistence.rb +28 -0
- data/lib/split/persistence/cookie_adapter.rb +94 -0
- data/lib/split/persistence/dual_adapter.rb +85 -0
- data/lib/split/persistence/redis_adapter.rb +57 -0
- data/lib/split/persistence/session_adapter.rb +29 -0
- data/lib/split/redis_interface.rb +50 -0
- data/lib/split/trial.rb +117 -0
- data/lib/split/user.rb +69 -0
- data/lib/split/version.rb +7 -0
- data/lib/split/zscore.rb +57 -0
- data/spec/algorithms/block_randomization_spec.rb +32 -0
- data/spec/algorithms/weighted_sample_spec.rb +19 -0
- data/spec/algorithms/whiplash_spec.rb +24 -0
- data/spec/alternative_spec.rb +320 -0
- data/spec/combined_experiments_helper_spec.rb +57 -0
- data/spec/configuration_spec.rb +258 -0
- data/spec/dashboard/pagination_helpers_spec.rb +200 -0
- data/spec/dashboard/paginator_spec.rb +37 -0
- data/spec/dashboard_helpers_spec.rb +42 -0
- data/spec/dashboard_spec.rb +210 -0
- data/spec/encapsulated_helper_spec.rb +52 -0
- data/spec/experiment_catalog_spec.rb +53 -0
- data/spec/experiment_spec.rb +533 -0
- data/spec/goals_collection_spec.rb +80 -0
- data/spec/helper_spec.rb +1111 -0
- data/spec/metric_spec.rb +31 -0
- data/spec/persistence/cookie_adapter_spec.rb +106 -0
- data/spec/persistence/dual_adapter_spec.rb +194 -0
- data/spec/persistence/redis_adapter_spec.rb +90 -0
- data/spec/persistence/session_adapter_spec.rb +32 -0
- data/spec/persistence_spec.rb +34 -0
- data/spec/redis_interface_spec.rb +111 -0
- data/spec/spec_helper.rb +52 -0
- data/spec/split_spec.rb +43 -0
- data/spec/support/cookies_mock.rb +20 -0
- data/spec/trial_spec.rb +299 -0
- data/spec/user_spec.rb +87 -0
- metadata +322 -0
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Split
|
3
|
+
class ExperimentCatalog
|
4
|
+
# Return all experiments
|
5
|
+
def self.all
|
6
|
+
# Call compact to prevent nil experiments from being returned -- seems to happen during gem upgrades
|
7
|
+
Split.redis.smembers(:experiments).map {|e| find(e)}.compact
|
8
|
+
end
|
9
|
+
|
10
|
+
# Return experiments without a winner (considered "active") first
|
11
|
+
def self.all_active_first
|
12
|
+
all.partition{|e| not e.winner}.map{|es| es.sort_by(&:name)}.flatten
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.find(name)
|
16
|
+
return unless Split.redis.exists(name)
|
17
|
+
Experiment.new(name).tap { |exp| exp.load_from_redis }
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.find_or_initialize(metric_descriptor, control = nil, *alternatives)
|
21
|
+
# Check if array is passed to ab_test
|
22
|
+
# e.g. ab_test('name', ['Alt 1', 'Alt 2', 'Alt 3'])
|
23
|
+
if control.is_a? Array and alternatives.length.zero?
|
24
|
+
control, alternatives = control.first, control[1..-1]
|
25
|
+
end
|
26
|
+
|
27
|
+
experiment_name_with_version, goals = normalize_experiment(metric_descriptor)
|
28
|
+
experiment_name = experiment_name_with_version.to_s.split(':')[0]
|
29
|
+
Split::Experiment.new(experiment_name,
|
30
|
+
:alternatives => [control].compact + alternatives, :goals => goals)
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.find_or_create(metric_descriptor, control = nil, *alternatives)
|
34
|
+
experiment = find_or_initialize(metric_descriptor, control, *alternatives)
|
35
|
+
experiment.save
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.normalize_experiment(metric_descriptor)
|
39
|
+
if Hash === metric_descriptor
|
40
|
+
experiment_name = metric_descriptor.keys.first
|
41
|
+
goals = Array(metric_descriptor.values.first)
|
42
|
+
else
|
43
|
+
experiment_name = metric_descriptor
|
44
|
+
goals = []
|
45
|
+
end
|
46
|
+
return experiment_name, goals
|
47
|
+
end
|
48
|
+
private_class_method :normalize_experiment
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
class String
|
3
|
+
# Constatntize is often provided by ActiveSupport, but ActiveSupport is not a dependency of Split.
|
4
|
+
unless method_defined?(:constantize)
|
5
|
+
def constantize
|
6
|
+
names = self.split('::')
|
7
|
+
names.shift if names.empty? || names.first.empty?
|
8
|
+
|
9
|
+
constant = Object
|
10
|
+
names.each do |name|
|
11
|
+
constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
|
12
|
+
end
|
13
|
+
constant
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Split
|
3
|
+
class GoalsCollection
|
4
|
+
|
5
|
+
def initialize(experiment_name, goals=nil)
|
6
|
+
@experiment_name = experiment_name
|
7
|
+
@goals = goals
|
8
|
+
end
|
9
|
+
|
10
|
+
def load_from_redis
|
11
|
+
Split.redis.lrange(goals_key, 0, -1)
|
12
|
+
end
|
13
|
+
|
14
|
+
def load_from_configuration
|
15
|
+
goals = Split.configuration.experiment_for(@experiment_name)[:goals]
|
16
|
+
|
17
|
+
if goals.nil?
|
18
|
+
goals = []
|
19
|
+
else
|
20
|
+
goals.flatten
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def save
|
25
|
+
return false if @goals.nil?
|
26
|
+
RedisInterface.new.persist_list(goals_key, @goals)
|
27
|
+
end
|
28
|
+
|
29
|
+
def validate!
|
30
|
+
unless @goals.nil? || @goals.kind_of?(Array)
|
31
|
+
raise ArgumentError, 'Goals must be an array'
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def delete
|
36
|
+
Split.redis.del(goals_key)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def goals_key
|
42
|
+
"#{@experiment_name}:goals"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/split/helper.rb
ADDED
@@ -0,0 +1,165 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Split
|
3
|
+
module Helper
|
4
|
+
OVERRIDE_PARAM_NAME = "ab_test"
|
5
|
+
|
6
|
+
module_function
|
7
|
+
|
8
|
+
def ab_test(metric_descriptor, control = nil, *alternatives)
|
9
|
+
begin
|
10
|
+
experiment = ExperimentCatalog.find_or_initialize(metric_descriptor, control, *alternatives)
|
11
|
+
alternative = if Split.configuration.enabled && !exclude_visitor?
|
12
|
+
experiment.save
|
13
|
+
raise(Split::InvalidExperimentsFormatError) unless (Split.configuration.experiments || {}).fetch(experiment.name.to_sym, {})[:combined_experiments].nil?
|
14
|
+
trial = Trial.new(:user => ab_user, :experiment => experiment,
|
15
|
+
:override => override_alternative(experiment.name), :exclude => exclude_visitor?,
|
16
|
+
:disabled => split_generically_disabled?)
|
17
|
+
alt = trial.choose!(self)
|
18
|
+
alt ? alt.name : nil
|
19
|
+
else
|
20
|
+
control_variable(experiment.control)
|
21
|
+
end
|
22
|
+
rescue Errno::ECONNREFUSED, Redis::BaseError, SocketError => e
|
23
|
+
raise(e) unless Split.configuration.db_failover
|
24
|
+
Split.configuration.db_failover_on_db_error.call(e)
|
25
|
+
|
26
|
+
if Split.configuration.db_failover_allow_parameter_override
|
27
|
+
alternative = override_alternative(experiment.name) if override_present?(experiment.name)
|
28
|
+
alternative = control_variable(experiment.control) if split_generically_disabled?
|
29
|
+
end
|
30
|
+
ensure
|
31
|
+
alternative ||= control_variable(experiment.control)
|
32
|
+
end
|
33
|
+
|
34
|
+
if block_given?
|
35
|
+
metadata = trial ? trial.metadata : {}
|
36
|
+
yield(alternative, metadata)
|
37
|
+
else
|
38
|
+
alternative
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def reset!(experiment)
|
43
|
+
ab_user.delete(experiment.key)
|
44
|
+
end
|
45
|
+
|
46
|
+
def finish_experiment(experiment, options = {:reset => true})
|
47
|
+
return false if active_experiments[experiment.name].nil?
|
48
|
+
return true if experiment.has_winner?
|
49
|
+
should_reset = experiment.resettable? && options[:reset]
|
50
|
+
if ab_user[experiment.finished_key] && !should_reset
|
51
|
+
return true
|
52
|
+
else
|
53
|
+
alternative_name = ab_user[experiment.key]
|
54
|
+
trial = Trial.new(:user => ab_user, :experiment => experiment,
|
55
|
+
:alternative => alternative_name)
|
56
|
+
trial.complete!(options[:goals], self)
|
57
|
+
|
58
|
+
if should_reset
|
59
|
+
reset!(experiment)
|
60
|
+
else
|
61
|
+
ab_user[experiment.finished_key] = true
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def ab_finished(metric_descriptor, options = {:reset => true})
|
67
|
+
return if exclude_visitor? || Split.configuration.disabled?
|
68
|
+
metric_descriptor, goals = normalize_metric(metric_descriptor)
|
69
|
+
experiments = Metric.possible_experiments(metric_descriptor)
|
70
|
+
|
71
|
+
if experiments.any?
|
72
|
+
experiments.each do |experiment|
|
73
|
+
finish_experiment(experiment, options.merge(:goals => goals))
|
74
|
+
end
|
75
|
+
end
|
76
|
+
rescue => e
|
77
|
+
raise unless Split.configuration.db_failover
|
78
|
+
Split.configuration.db_failover_on_db_error.call(e)
|
79
|
+
end
|
80
|
+
|
81
|
+
def ab_record_extra_info(metric_descriptor, key, value = 1)
|
82
|
+
return if exclude_visitor? || Split.configuration.disabled?
|
83
|
+
metric_descriptor, _ = normalize_metric(metric_descriptor)
|
84
|
+
experiments = Metric.possible_experiments(metric_descriptor)
|
85
|
+
|
86
|
+
if experiments.any?
|
87
|
+
experiments.each do |experiment|
|
88
|
+
alternative_name = ab_user[experiment.key]
|
89
|
+
|
90
|
+
if alternative_name
|
91
|
+
alternative = experiment.alternatives.find{|alt| alt.name == alternative_name}
|
92
|
+
alternative.record_extra_info(key, value) if alternative
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
rescue => e
|
97
|
+
raise unless Split.configuration.db_failover
|
98
|
+
Split.configuration.db_failover_on_db_error.call(e)
|
99
|
+
end
|
100
|
+
|
101
|
+
def ab_active_experiments()
|
102
|
+
ab_user.active_experiments
|
103
|
+
rescue => e
|
104
|
+
raise unless Split.configuration.db_failover
|
105
|
+
Split.configuration.db_failover_on_db_error.call(e)
|
106
|
+
end
|
107
|
+
|
108
|
+
|
109
|
+
def override_present?(experiment_name)
|
110
|
+
override_alternative(experiment_name)
|
111
|
+
end
|
112
|
+
|
113
|
+
def override_alternative(experiment_name)
|
114
|
+
defined?(params) && params[OVERRIDE_PARAM_NAME] && params[OVERRIDE_PARAM_NAME][experiment_name]
|
115
|
+
end
|
116
|
+
|
117
|
+
def split_generically_disabled?
|
118
|
+
defined?(params) && params['SPLIT_DISABLE']
|
119
|
+
end
|
120
|
+
|
121
|
+
def ab_user
|
122
|
+
@ab_user ||= User.new(self)
|
123
|
+
end
|
124
|
+
|
125
|
+
def exclude_visitor?
|
126
|
+
defined?(request) && (instance_exec(request, &Split.configuration.ignore_filter) || is_ignored_ip_address? || is_robot? || is_preview?)
|
127
|
+
end
|
128
|
+
|
129
|
+
def is_robot?
|
130
|
+
defined?(request) && request.user_agent =~ Split.configuration.robot_regex
|
131
|
+
end
|
132
|
+
|
133
|
+
def is_preview?
|
134
|
+
defined?(request) && defined?(request.headers) && request.headers['x-purpose'] == 'preview'
|
135
|
+
end
|
136
|
+
|
137
|
+
def is_ignored_ip_address?
|
138
|
+
return false if Split.configuration.ignore_ip_addresses.empty?
|
139
|
+
|
140
|
+
Split.configuration.ignore_ip_addresses.each do |ip|
|
141
|
+
return true if defined?(request) && (request.ip == ip || (ip.class == Regexp && request.ip =~ ip))
|
142
|
+
end
|
143
|
+
false
|
144
|
+
end
|
145
|
+
|
146
|
+
def active_experiments
|
147
|
+
ab_user.active_experiments
|
148
|
+
end
|
149
|
+
|
150
|
+
def normalize_metric(metric_descriptor)
|
151
|
+
if Hash === metric_descriptor
|
152
|
+
experiment_name = metric_descriptor.keys.first
|
153
|
+
goals = Array(metric_descriptor.values.first)
|
154
|
+
else
|
155
|
+
experiment_name = metric_descriptor
|
156
|
+
goals = []
|
157
|
+
end
|
158
|
+
return experiment_name, goals
|
159
|
+
end
|
160
|
+
|
161
|
+
def control_variable(control)
|
162
|
+
Hash === control ? control.keys.first.to_s : control.to_s
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
data/lib/split/metric.rb
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Split
|
3
|
+
class Metric
|
4
|
+
attr_accessor :name
|
5
|
+
attr_accessor :experiments
|
6
|
+
|
7
|
+
def initialize(attrs = {})
|
8
|
+
attrs.each do |key,value|
|
9
|
+
if self.respond_to?("#{key}=")
|
10
|
+
self.send("#{key}=", value)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.load_from_redis(name)
|
16
|
+
metric = Split.redis.hget(:metrics, name)
|
17
|
+
if metric
|
18
|
+
experiment_names = metric.split(',')
|
19
|
+
|
20
|
+
experiments = experiment_names.collect do |experiment_name|
|
21
|
+
Split::ExperimentCatalog.find(experiment_name)
|
22
|
+
end
|
23
|
+
|
24
|
+
Split::Metric.new(:name => name, :experiments => experiments)
|
25
|
+
else
|
26
|
+
nil
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.load_from_configuration(name)
|
31
|
+
metrics = Split.configuration.metrics
|
32
|
+
if metrics && metrics[name]
|
33
|
+
Split::Metric.new(:experiments => metrics[name], :name => name)
|
34
|
+
else
|
35
|
+
nil
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.find(name)
|
40
|
+
name = name.intern if name.is_a?(String)
|
41
|
+
metric = load_from_configuration(name)
|
42
|
+
metric = load_from_redis(name) if metric.nil?
|
43
|
+
metric
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.find_or_create(attrs)
|
47
|
+
metric = find(attrs[:name])
|
48
|
+
unless metric
|
49
|
+
metric = new(attrs)
|
50
|
+
metric.save
|
51
|
+
end
|
52
|
+
metric
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.all
|
56
|
+
redis_metrics = Split.redis.hgetall(:metrics).collect do |key, value|
|
57
|
+
find(key)
|
58
|
+
end
|
59
|
+
configuration_metrics = Split.configuration.metrics.collect do |key, value|
|
60
|
+
new(name: key, experiments: value)
|
61
|
+
end
|
62
|
+
redis_metrics | configuration_metrics
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.possible_experiments(metric_name)
|
66
|
+
experiments = []
|
67
|
+
metric = Split::Metric.find(metric_name)
|
68
|
+
if metric
|
69
|
+
experiments << metric.experiments
|
70
|
+
end
|
71
|
+
experiment = Split::ExperimentCatalog.find(metric_name)
|
72
|
+
if experiment
|
73
|
+
experiments << experiment
|
74
|
+
end
|
75
|
+
experiments.flatten
|
76
|
+
end
|
77
|
+
|
78
|
+
def save
|
79
|
+
Split.redis.hset(:metrics, name, experiments.map(&:name).join(','))
|
80
|
+
end
|
81
|
+
|
82
|
+
def complete!
|
83
|
+
experiments.each do |experiment|
|
84
|
+
experiment.complete!
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.normalize_metric(label)
|
89
|
+
if Hash === label
|
90
|
+
metric_name = label.keys.first
|
91
|
+
goals = label.values.first
|
92
|
+
else
|
93
|
+
metric_name = label
|
94
|
+
goals = []
|
95
|
+
end
|
96
|
+
return metric_name, goals
|
97
|
+
end
|
98
|
+
private_class_method :normalize_metric
|
99
|
+
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Split
|
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
|
+
|
10
|
+
ADAPTERS = {
|
11
|
+
:cookie => Split::Persistence::CookieAdapter,
|
12
|
+
:session => Split::Persistence::SessionAdapter
|
13
|
+
}.freeze
|
14
|
+
|
15
|
+
def self.adapter
|
16
|
+
if persistence_config.is_a?(Symbol)
|
17
|
+
ADAPTERS.fetch(persistence_config) { raise Split::InvalidPersistenceAdapterError }
|
18
|
+
else
|
19
|
+
persistence_config
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.persistence_config
|
24
|
+
Split.configuration.persistence
|
25
|
+
end
|
26
|
+
private_class_method :persistence_config
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "json"
|
3
|
+
|
4
|
+
module Split
|
5
|
+
module Persistence
|
6
|
+
class CookieAdapter
|
7
|
+
|
8
|
+
def initialize(context)
|
9
|
+
@context = context
|
10
|
+
@request, @response = context.request, context.response
|
11
|
+
@cookies = @request.cookies
|
12
|
+
@expires = Time.now + cookie_length_config
|
13
|
+
end
|
14
|
+
|
15
|
+
def [](key)
|
16
|
+
hash[key.to_s]
|
17
|
+
end
|
18
|
+
|
19
|
+
def []=(key, value)
|
20
|
+
set_cookie(hash.merge!(key.to_s => value))
|
21
|
+
end
|
22
|
+
|
23
|
+
def delete(key)
|
24
|
+
set_cookie(hash.tap { |h| h.delete(key.to_s) })
|
25
|
+
end
|
26
|
+
|
27
|
+
def keys
|
28
|
+
hash.keys
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
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")
|
69
|
+
end
|
70
|
+
|
71
|
+
def hash
|
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
|
80
|
+
{}
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def cookie_length_config
|
86
|
+
Split.configuration.persistence_cookie_length
|
87
|
+
end
|
88
|
+
|
89
|
+
def action_dispatch?
|
90
|
+
defined?(Rails) && @response.is_a?(ActionDispatch::Response)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|