trailguide 0.1.7 → 0.1.8
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/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
|