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 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