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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5adb31d9a97b2a73b6ff57b91d7f3907d408c511821b72fe0f7b1f857873a794
4
- data.tar.gz: 3e054cb7abcd176989b6a322f3f9b34e69d2b2b662824a2a84b4e2c3a6c1040a
3
+ metadata.gz: a43a9a982533a051bb03fb7c0d8dc4434be3b60e653fa8225ecb001268acd03f
4
+ data.tar.gz: c80071f1f4e4f0c442b3e875a77810d92c9ed775518b19f1f13390577fb1c1c0
5
5
  SHA512:
6
- metadata.gz: eeefcc9c9b4e08bfe433b576ca5da9614e6295ae354a4858811af3fad1660cd5c3acdecd8afeaa61ed17edbff34d7eebc2bd797ae8c421c199096c4402bc8d83
7
- data.tar.gz: 92d4106bff4e3ba5db67e4812b02029126ce3fb43790525c1be0a024fa5dae84cd4b44f4a75e361f4f1c605228bf8fb84e9fa371485ef77058bf3310b33042e2
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 do
4
- render json: { error: "Experiment does not exist" }, status: 404 and return unless experiment.present?
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
@@ -1,4 +1,6 @@
1
1
  TrailGuide::Engine.routes.draw do
2
+ get '/' => 'experiments#index',
3
+ defaults: { format: :json }
2
4
  get '/:experiment_name' => 'experiments#choose',
3
5
  defaults: { format: :json }
4
6
  match '/:experiment_name' => 'experiments#convert',
@@ -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 NoMethodError, "Your current context (#{context}) does not support cookies" unless context.respond_to?(:cookies, true)
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
- # TODO better error
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 NoMethodError, "Your current context (#{context}) does not support sessions" unless context.respond_to?(:session, true)
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
 
@@ -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 options[:control] if options[:control]
46
- algorithm options[:algorithm] if options[:algorithm]
47
- goals options[:goals] if options[:goals]
48
- metric options[:metric] if options[:metric]
49
- resettable options[:resettable] if options.key?(:resettable)
50
- allow_multiple_conversions options[:allow_multiple_conversions] if options.key?(:allow_multiple_conversions)
51
- allow_multiple_goals options[:allow_multiple_goals] if options.key?(:allow_multiple_goals)
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
- experiment_name name
60
- instance_eval &block
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
- # TODO could probably move all this configuration stuff at the class level
10
- # into a canfig object instead...?
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 config_algorithm
17
- config_algo = TrailGuide.configuration.algorithm
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
- if @resettable.nil?
44
- !TrailGuide.configuration.reset_manually
45
- else
46
- !!@resettable
47
- end
23
+ !configuration.reset_manually
48
24
  end
49
25
 
50
- def variant(name, metadata: {}, weight: 1, control: false)
51
- raise ArgumentError, "The variant #{name} already exists in experiment #{experiment_name}" if variants.any? { |var| var == name }
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
- @variants
32
+ configuration.variants
62
33
  else
63
- @variants.select { |var| !var.control? }
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, :storage_key,
256
- :started?, :started_at, :start!, :resettable?, :winner?, :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? && !TrailGuide.configuration.store_override
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 TrailGuide.configuration.track_override && started?
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? && TrailGuide.configuration.start_manually
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 ArgumentError, "You must provide a valid goal checkpoint for #{experiment_name}" unless checkpoint.present? || funnels.empty?
301
- raise ArgumentError, "Unknown goal checkpoint: #{checkpoint}" unless checkpoint.nil? || funnels.any? { |funnel| funnel == checkpoint.to_s.underscore.to_sym }
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
@@ -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 ArgumentError, "Please provide a single experiment" unless experiments.length == 1
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 NoMethodError,
78
- "You must define a controller method that matches variant `#{variant.name}` in your experiment `#{metric}`. In this case it looks like you need to define #{context.class.name}##{varmeth}(metadata={})"
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 NoMethodError,
81
- "You must define a helper method that matches variant `#{variant.name}` in your experiment `#{metric}`. In this case it looks like you need to define ApplicationHelper##{varmeth}(metadata={})"
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 NoMethodError,
84
- "You must define a method that matches variant `#{variant.name}` in your experiment `#{metric}`. In this case it looks like you need to define #{context.class.name}##{varmeth}(metadata={})"
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 NoMethodError, "The current context does not support rendering. Rendering is only available for controllers and views." unless context.respond_to?(:render, true)
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 ArgumentError, "This experiment does not have any defined goal checkpoints" unless checkpoint.nil?
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 ArgumentError, "Invalid goal checkpoint: #{checkpoint}" unless experiment.funnels.any? { |funnel| funnel == checkpoint.to_s.underscore.to_sym }
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
 
@@ -58,10 +58,10 @@ module TrailGuide
58
58
 
59
59
  def converted(checkpoint=nil)
60
60
  if experiment.funnels.empty?
61
- raise ArgumentError, "This experiment does not have any defined goal checkpoints" unless checkpoint.nil?
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 ArgumentError, "Invalid goal checkpoint: #{checkpoint}" unless experiment.funnels.any? { |funnel| funnel == checkpoint.to_s.underscore.to_sym }
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|
@@ -2,7 +2,7 @@ module TrailGuide
2
2
  module Version
3
3
  MAJOR = 0
4
4
  MINOR = 1
5
- PATCH = 7
5
+ PATCH = 8
6
6
  VERSION = "#{MAJOR}.#{MINOR}.#{PATCH}"
7
7
 
8
8
  class << self
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.7
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