ab-split 1.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 +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
|