trailguide 0.1.7 → 0.1.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/controllers/trail_guide/experiments_controller.rb +11 -2
- data/config/routes.rb +2 -0
- data/lib/trail_guide/adapters/participants/cookie.rb +1 -1
- data/lib/trail_guide/adapters/participants/redis.rb +1 -2
- data/lib/trail_guide/adapters/participants/session.rb +1 -1
- data/lib/trail_guide/engine.rb +13 -10
- data/lib/trail_guide/errors.rb +7 -0
- data/lib/trail_guide/experiment.rb +23 -143
- data/lib/trail_guide/experiment_config.rb +132 -0
- data/lib/trail_guide/helper.rb +9 -10
- data/lib/trail_guide/participant.rb +2 -2
- data/lib/trail_guide/variant.rb +2 -2
- data/lib/trail_guide/version.rb +1 -1
- data/lib/trailguide.rb +4 -1
- metadata +3 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a43a9a982533a051bb03fb7c0d8dc4434be3b60e653fa8225ecb001268acd03f
|
4
|
+
data.tar.gz: c80071f1f4e4f0c442b3e875a77810d92c9ed775518b19f1f13390577fb1c1c0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d084c8a7461256672372a1440e2a4c68d73ebe393acd8378346c45c1ad082aebe4b0d2346919e0f4f5324c09257bb525dda244fe0a72b9114e094e657d4938c1
|
7
|
+
data.tar.gz: f3ddb4f30ef7b0928f2851a42ce194c3b67bc46ea863821fd50154fecdf72b8b56a355bf6b7d2b2cb937d7b9d8f9029cf22f142d1f28c5eec7dbd13ce29e12bb
|
@@ -1,7 +1,12 @@
|
|
1
1
|
module TrailGuide
|
2
2
|
class ExperimentsController < ::ApplicationController
|
3
|
-
before_action
|
4
|
-
|
3
|
+
before_action :ensure_experiment, except: [:index]
|
4
|
+
|
5
|
+
def index
|
6
|
+
participant = trailguide.participant
|
7
|
+
render json: {
|
8
|
+
experiments: participant.active_experiments
|
9
|
+
}
|
5
10
|
end
|
6
11
|
|
7
12
|
def choose
|
@@ -39,5 +44,9 @@ module TrailGuide
|
|
39
44
|
def metadata
|
40
45
|
@metadata ||= params[:metadata].try(:permit!) || {}
|
41
46
|
end
|
47
|
+
|
48
|
+
def ensure_experiment
|
49
|
+
render json: { error: "Experiment does not exist" }, status: 404 and return false unless experiment.present?
|
50
|
+
end
|
42
51
|
end
|
43
52
|
end
|
data/config/routes.rb
CHANGED
@@ -26,7 +26,7 @@ module TrailGuide
|
|
26
26
|
|
27
27
|
# instance method, creates a new adapter and passes through config
|
28
28
|
def new(context)
|
29
|
-
raise
|
29
|
+
raise UnsupportedContextError, "Your current context (#{context}) does not support cookies" unless context.respond_to?(:cookies, true)
|
30
30
|
Adapter.new(context, configuration)
|
31
31
|
end
|
32
32
|
|
@@ -43,8 +43,7 @@ module TrailGuide
|
|
43
43
|
end
|
44
44
|
@storage_key = "#{config.namespace}:#{key}"
|
45
45
|
else
|
46
|
-
|
47
|
-
raise ArgumentError, "You must configure a `lookup` proc"
|
46
|
+
raise ArgumentError, "You must configure a `lookup` proc to use the redis adapter."
|
48
47
|
end
|
49
48
|
end
|
50
49
|
|
@@ -21,7 +21,7 @@ module TrailGuide
|
|
21
21
|
|
22
22
|
# instance method, creates a new adapter and passes through config
|
23
23
|
def new(context)
|
24
|
-
raise
|
24
|
+
raise UnsupportedContextError, "Your current context (#{context}) does not support sessions" unless context.respond_to?(:session, true)
|
25
25
|
Adapter.new(context, configuration)
|
26
26
|
end
|
27
27
|
|
data/lib/trail_guide/engine.rb
CHANGED
@@ -38,17 +38,20 @@ module TrailGuide
|
|
38
38
|
end
|
39
39
|
end
|
40
40
|
|
41
|
-
DSL.experiment(name) do
|
41
|
+
DSL.experiment(name) do |config|
|
42
42
|
expvars.each do |expvar|
|
43
43
|
variant *expvar
|
44
44
|
end
|
45
|
-
control
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
45
|
+
config.control = options[:control] if options[:control]
|
46
|
+
config.metric = options[:metric] if options[:metric]
|
47
|
+
config.algorithm = options[:algorithm] if options[:algorithm]
|
48
|
+
config.goals = options[:goals] if options[:goals]
|
49
|
+
config.reset_manually = options[:reset_manually] if options.key?(:reset_manually)
|
50
|
+
config.start_manually = options[:start_manually] if options.key?(:start_manually)
|
51
|
+
config.store_override = options[:store_override] if options.key?(:store_override)
|
52
|
+
config.track_override = options[:track_override] if options.key?(:track_override)
|
53
|
+
config.allow_multiple_conversions = options[:allow_multiple_conversions] if options.key?(:allow_multiple_conversions)
|
54
|
+
config.allow_multiple_goals = options[:allow_multiple_goals] if options.key?(:allow_multiple_goals)
|
52
55
|
end
|
53
56
|
end
|
54
57
|
end
|
@@ -56,8 +59,8 @@ module TrailGuide
|
|
56
59
|
class DSL
|
57
60
|
def self.experiment(name, &block)
|
58
61
|
Class.new(TrailGuide::Experiment) do
|
59
|
-
|
60
|
-
|
62
|
+
configure name: name
|
63
|
+
configure &block
|
61
64
|
end
|
62
65
|
end
|
63
66
|
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
module TrailGuide
|
2
|
+
class InvalidGoalError < ArgumentError; end
|
3
|
+
class UnsupportedContextError < NoMethodError; end
|
4
|
+
class NoExperimentsError < ArgumentError; end
|
5
|
+
class TooManyExperimentsError < ArgumentError; end
|
6
|
+
class NoVariantMethodError < NoMethodError; end
|
7
|
+
end
|
@@ -1,160 +1,40 @@
|
|
1
|
+
require "trail_guide/experiment_config"
|
2
|
+
|
1
3
|
module TrailGuide
|
2
4
|
class Experiment
|
3
5
|
class << self
|
6
|
+
delegate :metric, :algorithm, :control, :goals, :callbacks,
|
7
|
+
:allow_multiple_conversions?, :allow_multiple_goals?, to: :configuration
|
8
|
+
alias_method :funnels, :goals
|
9
|
+
|
4
10
|
def inherited(child)
|
5
|
-
# TODO allow inheriting algo, variants, goals, metrics, etc.
|
6
11
|
TrailGuide::Catalog.register(child)
|
7
12
|
end
|
8
13
|
|
9
|
-
|
10
|
-
|
11
|
-
def experiment_name(name=nil)
|
12
|
-
@experiment_name = name.to_s.underscore.to_sym unless name.nil?
|
13
|
-
@experiment_name || self.name.try(:underscore).try(:to_sym)
|
14
|
+
def configuration
|
15
|
+
@configuration ||= ExperimentConfig.new(self)
|
14
16
|
end
|
15
17
|
|
16
|
-
def
|
17
|
-
|
18
|
-
case config_algo
|
19
|
-
when :weighted
|
20
|
-
config_algo = TrailGuide::Algorithms::Weighted
|
21
|
-
when :bandit
|
22
|
-
config_algo = TrailGuide::Algorithms::Bandit
|
23
|
-
when :distributed
|
24
|
-
config_algo = TrailGuide::Algorithms::Distributed
|
25
|
-
when :random
|
26
|
-
config_algo = TrailGuide::Algorithms::Random
|
27
|
-
else
|
28
|
-
config_algo = config_algo.constantize if config_algo.is_a?(String)
|
29
|
-
end
|
30
|
-
config_algo
|
31
|
-
end
|
32
|
-
|
33
|
-
def algorithm(algo=nil)
|
34
|
-
@algorithm = TrailGuide::Algorithms.algorithm(algo) unless algo.nil?
|
35
|
-
@algorithm ||= TrailGuide::Algorithms.algorithm(TrailGuide.configuration.algorithm)
|
36
|
-
end
|
37
|
-
|
38
|
-
def resettable(reset)
|
39
|
-
@resettable = reset
|
18
|
+
def configure(*args, &block)
|
19
|
+
configuration.configure(*args, &block)
|
40
20
|
end
|
41
21
|
|
42
22
|
def resettable?
|
43
|
-
|
44
|
-
!TrailGuide.configuration.reset_manually
|
45
|
-
else
|
46
|
-
!!@resettable
|
47
|
-
end
|
23
|
+
!configuration.reset_manually
|
48
24
|
end
|
49
25
|
|
50
|
-
def
|
51
|
-
|
52
|
-
control = true if variants.empty?
|
53
|
-
variant = Variant.new(self, name, metadata: metadata, weight: weight, control: control)
|
54
|
-
variants << variant
|
55
|
-
variant
|
26
|
+
def experiment_name
|
27
|
+
configuration.name
|
56
28
|
end
|
57
29
|
|
58
30
|
def variants(include_control=true)
|
59
|
-
@variants ||= []
|
60
31
|
if include_control
|
61
|
-
|
32
|
+
configuration.variants
|
62
33
|
else
|
63
|
-
|
34
|
+
configuration.variants.select { |var| !var.control? }
|
64
35
|
end
|
65
36
|
end
|
66
37
|
|
67
|
-
def control(name=nil)
|
68
|
-
return variants.find { |var| var.control? } || variants.first if name.nil?
|
69
|
-
|
70
|
-
variants.each(&:variant!)
|
71
|
-
var_idx = variants.index { |var| var == name }
|
72
|
-
|
73
|
-
if var_idx.nil?
|
74
|
-
variant = Variant.new(self, name, control: true)
|
75
|
-
else
|
76
|
-
variant = variants.slice!(var_idx, 1)[0]
|
77
|
-
variant.control!
|
78
|
-
end
|
79
|
-
|
80
|
-
variants.unshift(variant)
|
81
|
-
return variant
|
82
|
-
end
|
83
|
-
|
84
|
-
def funnel(name)
|
85
|
-
funnels << name.to_s.underscore.to_sym
|
86
|
-
end
|
87
|
-
alias_method :goal, :funnel
|
88
|
-
|
89
|
-
def funnels(arr=nil)
|
90
|
-
@funnels = arr unless arr.nil?
|
91
|
-
@funnels ||= []
|
92
|
-
end
|
93
|
-
alias_method :goals, :funnels
|
94
|
-
|
95
|
-
def metric(key=nil)
|
96
|
-
@metric = key.to_s.underscore.to_sym unless key.nil?
|
97
|
-
@metric ||= experiment_name
|
98
|
-
end
|
99
|
-
|
100
|
-
def allow_multiple_conversions(allow)
|
101
|
-
@allow_multiple_conversions = allow
|
102
|
-
end
|
103
|
-
|
104
|
-
def allow_multiple_conversions?
|
105
|
-
!!@allow_multiple_conversions
|
106
|
-
end
|
107
|
-
|
108
|
-
def allow_multiple_goals(allow)
|
109
|
-
@allow_multiple_goals = allow
|
110
|
-
end
|
111
|
-
|
112
|
-
def allow_multiple_goals?
|
113
|
-
!!@allow_multiple_goals
|
114
|
-
end
|
115
|
-
|
116
|
-
def callbacks
|
117
|
-
@callbacks ||= begin
|
118
|
-
callbacks = {
|
119
|
-
on_choose: [TrailGuide.configuration.on_experiment_choose].compact,
|
120
|
-
on_use: [TrailGuide.configuration.on_experiment_use].compact,
|
121
|
-
on_convert: [TrailGuide.configuration.on_experiment_convert].compact,
|
122
|
-
on_start: [TrailGuide.configuration.on_experiment_start].compact,
|
123
|
-
on_stop: [TrailGuide.configuration.on_experiment_stop].compact,
|
124
|
-
on_reset: [TrailGuide.configuration.on_experiment_reset].compact,
|
125
|
-
on_delete: [TrailGuide.configuration.on_experiment_delete].compact,
|
126
|
-
}
|
127
|
-
end
|
128
|
-
end
|
129
|
-
|
130
|
-
def on_choose(meth=nil, &block)
|
131
|
-
callbacks[:on_choose] << (meth || block)
|
132
|
-
end
|
133
|
-
|
134
|
-
def on_use(meth=nil, &block)
|
135
|
-
callbacks[:on_use] << (meth || block)
|
136
|
-
end
|
137
|
-
|
138
|
-
def on_convert(meth=nil, &block)
|
139
|
-
callbacks[:on_convert] << (meth || block)
|
140
|
-
end
|
141
|
-
|
142
|
-
def on_start(meth=nil, &block)
|
143
|
-
callbacks[:on_start] << (meth || block)
|
144
|
-
end
|
145
|
-
|
146
|
-
def on_stop(meth=nil, &block)
|
147
|
-
callbacks[:on_stop] << (meth || block)
|
148
|
-
end
|
149
|
-
|
150
|
-
def on_reset(meth=nil, &block)
|
151
|
-
callbacks[:on_reset] << (meth || block)
|
152
|
-
end
|
153
|
-
|
154
|
-
def on_delete(meth=nil, &block)
|
155
|
-
callbacks[:on_delete] << (meth || block)
|
156
|
-
end
|
157
|
-
|
158
38
|
def run_callbacks(hook, *args)
|
159
39
|
return unless callbacks[hook]
|
160
40
|
args.unshift(self)
|
@@ -252,9 +132,9 @@ module TrailGuide
|
|
252
132
|
end
|
253
133
|
|
254
134
|
attr_reader :participant
|
255
|
-
delegate :experiment_name, :variants, :control, :funnels,
|
256
|
-
:started?, :started_at, :start!, :resettable?, :winner?,
|
257
|
-
:allow_multiple_conversions?, :allow_multiple_goals?, :callbacks,
|
135
|
+
delegate :configuration, :experiment_name, :variants, :control, :funnels,
|
136
|
+
:storage_key, :started?, :started_at, :start!, :resettable?, :winner?,
|
137
|
+
:winner, :allow_multiple_conversions?, :allow_multiple_goals?, :callbacks,
|
258
138
|
to: :class
|
259
139
|
|
260
140
|
def initialize(participant)
|
@@ -269,7 +149,7 @@ module TrailGuide
|
|
269
149
|
return control if TrailGuide.configuration.disabled
|
270
150
|
|
271
151
|
variant = choose_variant!(override: override, metadata: metadata, **opts)
|
272
|
-
participant.participating!(variant) unless override.present? && !
|
152
|
+
participant.participating!(variant) unless override.present? && !configuration.store_override
|
273
153
|
run_callbacks(:on_use, variant, metadata)
|
274
154
|
variant
|
275
155
|
end
|
@@ -278,11 +158,11 @@ module TrailGuide
|
|
278
158
|
return control if TrailGuide.configuration.disabled
|
279
159
|
if override.present?
|
280
160
|
variant = variants.find { |var| var == override }
|
281
|
-
return variant unless
|
161
|
+
return variant unless configuration.track_override && started?
|
282
162
|
else
|
283
163
|
return winner if winner?
|
284
164
|
return control if excluded
|
285
|
-
return control if !started? &&
|
165
|
+
return control if !started? && configuration.start_manually
|
286
166
|
start! unless started?
|
287
167
|
return variants.find { |var| var == participant[storage_key] } if participating?
|
288
168
|
return control unless TrailGuide.configuration.allow_multiple_experiments == true || !participant.participating_in_active_experiments?(TrailGuide.configuration.allow_multiple_experiments == false)
|
@@ -297,8 +177,8 @@ module TrailGuide
|
|
297
177
|
|
298
178
|
def convert!(checkpoint=nil, metadata: nil)
|
299
179
|
return false unless participating?
|
300
|
-
raise
|
301
|
-
raise
|
180
|
+
raise InvalidGoalError, "Invalid goal checkpoint `#{checkpoint}` for `#{experiment_name}`." unless checkpoint.present? || funnels.empty?
|
181
|
+
raise InvalidGoalError, "Invalid goal checkpoint `#{checkpoint}` for `#{experiment_name}`." unless checkpoint.nil? || funnels.any? { |funnel| funnel == checkpoint.to_s.underscore.to_sym }
|
302
182
|
# TODO eventually allow progressing through funnel checkpoints towards goals
|
303
183
|
if converted?(checkpoint)
|
304
184
|
return false unless allow_multiple_conversions?
|
@@ -0,0 +1,132 @@
|
|
1
|
+
module TrailGuide
|
2
|
+
class ExperimentConfig < Canfig::Config
|
3
|
+
ENGINE_CONFIG_KEYS = [
|
4
|
+
:start_manually, :reset_manually, :store_override, :track_override,
|
5
|
+
:algorithm, :allow_multiple_conversions, :allow_multiple_goals
|
6
|
+
].freeze
|
7
|
+
|
8
|
+
def self.engine_config
|
9
|
+
ENGINE_CONFIG_KEYS.map do |key|
|
10
|
+
[key, TrailGuide.configuration.send(key.to_sym)]
|
11
|
+
end.to_h
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.default_config
|
15
|
+
{ name: nil, metric: nil, variants: [], goals: [] }
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.callbacks_config
|
19
|
+
{
|
20
|
+
callbacks: {
|
21
|
+
on_choose: [TrailGuide.configuration.on_experiment_choose].flatten.compact,
|
22
|
+
on_use: [TrailGuide.configuration.on_experiment_use].flatten.compact,
|
23
|
+
on_convert: [TrailGuide.configuration.on_experiment_convert].flatten.compact,
|
24
|
+
on_start: [TrailGuide.configuration.on_experiment_start].flatten.compact,
|
25
|
+
on_stop: [TrailGuide.configuration.on_experiment_stop].flatten.compact,
|
26
|
+
on_reset: [TrailGuide.configuration.on_experiment_reset].flatten.compact,
|
27
|
+
on_delete: [TrailGuide.configuration.on_experiment_delete].flatten.compact,
|
28
|
+
}
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
attr_reader :experiment
|
33
|
+
|
34
|
+
def initialize(experiment, *args, **opts, &block)
|
35
|
+
@experiment = experiment
|
36
|
+
opts = opts.merge(self.class.engine_config)
|
37
|
+
opts = opts.merge(self.class.default_config)
|
38
|
+
opts = opts.merge(self.class.callbacks_config)
|
39
|
+
super(*args, **opts, &block)
|
40
|
+
end
|
41
|
+
|
42
|
+
def resettable?
|
43
|
+
!reset_manually
|
44
|
+
end
|
45
|
+
|
46
|
+
def allow_multiple_conversions?
|
47
|
+
allow_multiple_conversions
|
48
|
+
end
|
49
|
+
|
50
|
+
def allow_multiple_goals?
|
51
|
+
allow_multiple_goals
|
52
|
+
end
|
53
|
+
|
54
|
+
def name
|
55
|
+
@name ||= (self[:name] || experiment.name).try(:to_s).try(:underscore).try(:to_sym)
|
56
|
+
end
|
57
|
+
|
58
|
+
def metric
|
59
|
+
@metric ||= (self[:metric] || name).try(:to_s).try(:underscore).try(:to_sym)
|
60
|
+
end
|
61
|
+
|
62
|
+
def algorithm
|
63
|
+
@algorithm ||= TrailGuide::Algorithms.algorithm(self[:algorithm])
|
64
|
+
end
|
65
|
+
|
66
|
+
def variant(varname, metadata: {}, weight: 1, control: false)
|
67
|
+
raise ArgumentError, "The variant `#{varname}` already exists in the experiment `#{name}`" if variants.any? { |var| var == varname }
|
68
|
+
control = true if variants.empty?
|
69
|
+
variants.each(&:variant!) if control
|
70
|
+
variant = Variant.new(experiment, varname, metadata: metadata, weight: weight, control: control)
|
71
|
+
variants << variant
|
72
|
+
variant
|
73
|
+
end
|
74
|
+
|
75
|
+
def control
|
76
|
+
return variants.find { |var| var.control? } || variants.first
|
77
|
+
end
|
78
|
+
|
79
|
+
def control=(name)
|
80
|
+
variants.each(&:variant!)
|
81
|
+
var_idx = variants.index { |var| var == name }
|
82
|
+
|
83
|
+
if var_idx.nil?
|
84
|
+
variant = Variant.new(experiment, name, control: true)
|
85
|
+
else
|
86
|
+
variant = variants.slice!(var_idx, 1)[0]
|
87
|
+
variant.control!
|
88
|
+
end
|
89
|
+
|
90
|
+
variants.unshift(variant)
|
91
|
+
variant
|
92
|
+
end
|
93
|
+
|
94
|
+
def goal(name)
|
95
|
+
goals << name.to_s.underscore.to_sym
|
96
|
+
end
|
97
|
+
alias_method :funnel, :goal
|
98
|
+
|
99
|
+
def goals
|
100
|
+
self[:goals]
|
101
|
+
end
|
102
|
+
alias_method :funnels, :goals
|
103
|
+
|
104
|
+
def on_choose(meth=nil, &block)
|
105
|
+
callbacks[:on_choose] << (meth || block)
|
106
|
+
end
|
107
|
+
|
108
|
+
def on_use(meth=nil, &block)
|
109
|
+
callbacks[:on_use] << (meth || block)
|
110
|
+
end
|
111
|
+
|
112
|
+
def on_convert(meth=nil, &block)
|
113
|
+
callbacks[:on_convert] << (meth || block)
|
114
|
+
end
|
115
|
+
|
116
|
+
def on_start(meth=nil, &block)
|
117
|
+
callbacks[:on_start] << (meth || block)
|
118
|
+
end
|
119
|
+
|
120
|
+
def on_stop(meth=nil, &block)
|
121
|
+
callbacks[:on_stop] << (meth || block)
|
122
|
+
end
|
123
|
+
|
124
|
+
def on_reset(meth=nil, &block)
|
125
|
+
callbacks[:on_reset] << (meth || block)
|
126
|
+
end
|
127
|
+
|
128
|
+
def on_delete(meth=nil, &block)
|
129
|
+
callbacks[:on_delete] << (meth || block)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
data/lib/trail_guide/helper.rb
CHANGED
@@ -53,10 +53,11 @@ module TrailGuide
|
|
53
53
|
def initialize(context, metric, **opts)
|
54
54
|
super(context, **opts)
|
55
55
|
@metric = metric
|
56
|
+
raise NoExperimentsError, "Could not find any experiments matching `#{metric}`." if experiments.empty?
|
56
57
|
end
|
57
58
|
|
58
59
|
def choose!(**opts, &block)
|
59
|
-
raise
|
60
|
+
raise TooManyExperimentsError, "Selecting a variant requires a single experiment, but the metric `#{metric}` matches more than one experiment." if experiments.length > 1
|
60
61
|
opts = {override: override_variant, excluded: exclude_visitor?}.merge(opts)
|
61
62
|
variant = experiment.choose!(**opts)
|
62
63
|
if block_given?
|
@@ -67,21 +68,20 @@ module TrailGuide
|
|
67
68
|
end
|
68
69
|
|
69
70
|
def run!(methods: nil, **opts)
|
70
|
-
raise ArgumentError, "Please provide a single experiment" unless experiments.length == 1
|
71
71
|
choose!(**opts) do |variant, metadata|
|
72
72
|
varmeth = methods[variant.name] if methods
|
73
73
|
varmeth ||= variant.name
|
74
74
|
|
75
75
|
unless context.respond_to?(varmeth, true)
|
76
76
|
if context_type == :controller
|
77
|
-
raise
|
78
|
-
"You must define a controller method
|
77
|
+
raise NoVariantMethodError,
|
78
|
+
"Undefined local method `#{varmeth}`. You must define a controller method matching the variant `#{variant.name}` in your experiment `#{metric}`. In this case it looks like you need to define #{context.class.name}##{varmeth}(metadata={})"
|
79
79
|
elsif context_type == :template
|
80
|
-
raise
|
81
|
-
"You must define a helper method
|
80
|
+
raise NoVariantMethodError,
|
81
|
+
"Undefined local method `#{varmeth}`. You must define a helper method matching the variant `#{variant.name}` in your experiment `#{metric}`. In this case it looks like you need to define ApplicationHelper##{varmeth}(metadata={})"
|
82
82
|
else
|
83
|
-
raise
|
84
|
-
"You must define a method
|
83
|
+
raise NoVariantMethodError,
|
84
|
+
"Undefined local method `#{varmeth}`. You must define a method matching the variant `#{variant.name}` in your experiment `#{metric}`. In this case it looks like you need to define #{context.class.name}##{varmeth}(metadata={})"
|
85
85
|
end
|
86
86
|
end
|
87
87
|
|
@@ -97,8 +97,7 @@ module TrailGuide
|
|
97
97
|
end
|
98
98
|
|
99
99
|
def render!(prefix: nil, templates: nil, **opts)
|
100
|
-
raise
|
101
|
-
raise ArgumentError, "Please provide a single experiment" unless experiments.length == 1
|
100
|
+
raise UnsupportedContextError, "The current context (#{context}) does not support rendering. Rendering is only available in controllers and views." unless context.respond_to?(:render, true)
|
102
101
|
choose!(**opts) do |variant, metadata|
|
103
102
|
locals = { variant: variant, metadata: variant.metadata }
|
104
103
|
locals = { locals: locals } if context_type == :controller
|
@@ -42,14 +42,14 @@ module TrailGuide
|
|
42
42
|
|
43
43
|
def converted?(experiment, checkpoint=nil)
|
44
44
|
if experiment.funnels.empty?
|
45
|
-
raise
|
45
|
+
raise InvalidGoalError, "You provided the checkpoint `#{checkpoint}` but the experiment `#{experiment.experiment_name}` does not have any goals defined." unless checkpoint.nil?
|
46
46
|
storage_key = "#{experiment.storage_key}:converted"
|
47
47
|
return false unless adapter.key?(storage_key)
|
48
48
|
|
49
49
|
converted_at = Time.at(adapter[storage_key].to_i)
|
50
50
|
converted_at >= experiment.started_at
|
51
51
|
elsif !checkpoint.nil?
|
52
|
-
raise
|
52
|
+
raise InvalidGoalError, "Invalid goal checkpoint `#{checkpoint}` for experiment `#{experiment.experiment_name}`." unless experiment.funnels.any? { |funnel| funnel == checkpoint.to_s.underscore.to_sym }
|
53
53
|
storage_key = "#{experiment.storage_key}:#{checkpoint.to_s.underscore}"
|
54
54
|
return false unless adapter.key?(storage_key)
|
55
55
|
|
data/lib/trail_guide/variant.rb
CHANGED
@@ -58,10 +58,10 @@ module TrailGuide
|
|
58
58
|
|
59
59
|
def converted(checkpoint=nil)
|
60
60
|
if experiment.funnels.empty?
|
61
|
-
raise
|
61
|
+
raise InvalidGoalError, "You provided the checkpoint `#{checkpoint}` but the experiment `#{experiment.experiment_name}` does not have any goals defined." unless checkpoint.nil?
|
62
62
|
(TrailGuide.redis.hget(storage_key, 'converted') || 0).to_i
|
63
63
|
elsif !checkpoint.nil?
|
64
|
-
raise
|
64
|
+
raise InvalidGoalError, "Invalid goal checkpoint `#{checkpoint}` for experiment `#{experiment.experiment_name}`." unless experiment.funnels.any? { |funnel| funnel == checkpoint.to_s.underscore.to_sym }
|
65
65
|
(TrailGuide.redis.hget(storage_key, checkpoint.to_s.underscore) || 0).to_i
|
66
66
|
else
|
67
67
|
experiment.funnels.sum do |checkpoint|
|
data/lib/trail_guide/version.rb
CHANGED
data/lib/trailguide.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require "canfig"
|
2
2
|
require "redis"
|
3
|
+
require "trail_guide/errors"
|
3
4
|
require "trail_guide/adapters"
|
4
5
|
require "trail_guide/algorithms"
|
5
6
|
require "trail_guide/participant"
|
@@ -20,9 +21,11 @@ module TrailGuide
|
|
20
21
|
config.store_override = false
|
21
22
|
config.track_override = false
|
22
23
|
config.override_parameter = :experiment
|
23
|
-
config.allow_multiple_experiments = true # false / :control
|
24
24
|
config.algorithm = :weighted
|
25
25
|
config.adapter = :multi
|
26
|
+
config.allow_multiple_experiments = true # false / :control
|
27
|
+
config.allow_multiple_conversions = false
|
28
|
+
config.allow_multiple_goals = false
|
26
29
|
|
27
30
|
config.on_experiment_choose = nil # -> (experiment, variant, metadata) { ... }
|
28
31
|
config.on_experiment_use = nil # -> (experiment, variant, metadata) { ... }
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: trailguide
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mark Rebec
|
@@ -130,7 +130,9 @@ files:
|
|
130
130
|
- lib/trail_guide/algorithms/weighted.rb
|
131
131
|
- lib/trail_guide/catalog.rb
|
132
132
|
- lib/trail_guide/engine.rb
|
133
|
+
- lib/trail_guide/errors.rb
|
133
134
|
- lib/trail_guide/experiment.rb
|
135
|
+
- lib/trail_guide/experiment_config.rb
|
134
136
|
- lib/trail_guide/helper.rb
|
135
137
|
- lib/trail_guide/participant.rb
|
136
138
|
- lib/trail_guide/unity.rb
|