trailguide 0.1.19 → 0.1.20

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: 0128e341d1693238ba28d518b32157d62c6d2ae1ea727443c038b5b5f57a116e
4
- data.tar.gz: 2d67fe0ee2d33296c95c9040211c3c40a147c9ee99a6b2343977ab42209842d7
3
+ metadata.gz: c45506f04eec39f23d00fce31f84d1d1e24f4289e2870af413f8928d54cceef8
4
+ data.tar.gz: 1b6cb001e0fa544f6390e4a82e265922f9ab75b602f4406a2cddf337704fd076
5
5
  SHA512:
6
- metadata.gz: f1181502a72dab5945d8ccc9c077367062bd4853fc77566036e52643f9df8c5f8b8870a65d201ec542dbf60c210876317c5ab5f9ff3c4d4b1b050b7eaf76bd0d
7
- data.tar.gz: dc1791251621abf2c869300f81355aec51285224ebd2272e556388f19e713d7ffcc02df047d7f67ac2204bc77b050ff17d5b4bd006c42b079781c1fa55c3825a
6
+ metadata.gz: 757e5c2ad76a1143db739638fc60ccaf263d047c2ea3f41172c74368c8c3a2744ba760b0255c6393623ba2266df5e6ec4bff3f3c4e6898370eeefb536b7b5e0b
7
+ data.tar.gz: 820c4664c60c161dfad8e81dcab8cb61e4137d9961c97e5b0da3f9dfdb5e496692d3bc1bd7f9784645095dfb4be38c75bb73604b80e6e855d42ec2bdac187de0
@@ -9,6 +9,7 @@ TrailGuide.configure do |config|
9
9
 
10
10
  # request param for overriding/previewing variants - allows previewing
11
11
  # variants with request params
12
+ # i.e. example.com/somepage/?experiment[my_experiment]=option_b
12
13
  config.override_parameter = :experiment
13
14
 
14
15
  # whether or not participants are allowed to enter variant groups in multiple
@@ -27,7 +28,7 @@ TrailGuide.configure do |config|
27
28
  # in the control group for all except potentially one)
28
29
  config.allow_multiple_experiments = false
29
30
 
30
- # the participant adapter for users
31
+ # the participant adapter for storing experiment sessions
31
32
  #
32
33
  # :redis uses redis to persist user participation
33
34
  # :cookie uses a cookie to persist user participation
@@ -194,6 +195,25 @@ TrailGuide::Experiment.configure do |config|
194
195
  # config.on_convert = -> (experiment, variant, checkpoint, metadata) { ... }
195
196
 
196
197
 
198
+ # callback that can short-circuit participation based on your own logic, which
199
+ # gets called *after* all the core engine checks (i.e. that the user is
200
+ # not excluded or already participating, etc.)
201
+ #
202
+ # should return true or false
203
+ #
204
+ # config.allow_participation = -> (experiment, metadata) { ... return true }
205
+
206
+
207
+ # callback that can short-circuit conversion based on your own logic, which
208
+ # gets called *after* all the core engine checks (i.e. that the user is
209
+ # participating in the experiment, is within the bounds of the experiment
210
+ # configuration for allow_multiple_*, etc.)
211
+ #
212
+ # should return true or false
213
+ #
214
+ # config.allow_conversion = -> (experiment, checkpoint, metadata) { ... return true }
215
+
216
+
197
217
  # callback that can be used to modify the rollout of a selected winner - for
198
218
  # example you could use a custom algorithm or even something like the flipper
199
219
  # gem to do a "feature rollout" from your control variant to your winner for
@@ -203,3 +223,10 @@ TrailGuide::Experiment.configure do |config|
203
223
  #
204
224
  # config.rollout_winner = -> (experiment, winner) { ... return variant }
205
225
  end
226
+
227
+ # admin ui configuration
228
+ #
229
+ TrailGuide::Admin.configure do |config|
230
+ config.title = 'TrailGuide'
231
+ config.subtitle = 'Experiments and A/B Tests'
232
+ end
@@ -12,22 +12,20 @@ TrailGuide::Engine.routes.draw do
12
12
  via: [:put]
13
13
  end
14
14
 
15
- if defined?(TrailGuide::Admin::Engine)
16
- TrailGuide::Admin::Engine.routes.draw do
17
- resources :experiments, path: '/', only: [:index] do
18
- member do
19
- match :start, via: [:put, :post, :get]
20
- match :stop, via: [:put, :post, :get]
21
- match :reset, via: [:put, :post, :get]
22
- match :resume, via: [:put, :post, :get]
23
- match :restart, via: [:put, :post, :get]
15
+ TrailGuide::Admin::Engine.routes.draw do
16
+ resources :experiments, path: '/', only: [:index] do
17
+ member do
18
+ match :start, via: [:put, :post, :get]
19
+ match :stop, via: [:put, :post, :get]
20
+ match :reset, via: [:put, :post, :get]
21
+ match :resume, via: [:put, :post, :get]
22
+ match :restart, via: [:put, :post, :get]
24
23
 
25
- match :join, via: [:put, :post, :get], path: 'join/:variant'
26
- match :leave, via: [:put, :post, :get]
24
+ match :join, via: [:put, :post, :get], path: 'join/:variant'
25
+ match :leave, via: [:put, :post, :get]
27
26
 
28
- match :winner, via: [:put, :post, :get], path: 'winner/:variant'
29
- match :clear, via: [:put, :post, :get]
30
- end
27
+ match :winner, via: [:put, :post, :get], path: 'winner/:variant'
28
+ match :clear, via: [:put, :post, :get]
31
29
  end
32
30
  end
33
31
  end
@@ -3,10 +3,5 @@ require "trail_guide/admin/engine"
3
3
  module TrailGuide
4
4
  module Admin
5
5
  include Canfig::Module
6
-
7
- configure do |config|
8
- config.title = 'TrailGuide'
9
- config.subtitle = 'Experiments and A/B Tests'
10
- end
11
6
  end
12
7
  end
@@ -157,10 +157,9 @@ module TrailGuide
157
157
  end
158
158
 
159
159
  class DSL
160
- def self.experiment(name, &block)
160
+ def self.experiment(name, **opts, &block)
161
161
  Class.new(TrailGuide::Experiment) do
162
- configure name: name
163
- configure &block
162
+ configure opts.merge({name: name}), &block
164
163
  end
165
164
  end
166
165
  end
@@ -1,4 +1,5 @@
1
1
  require "trail_guide/experiments/config"
2
+ require "trail_guide/experiments/participant"
2
3
 
3
4
  module TrailGuide
4
5
  module Experiments
@@ -76,11 +77,11 @@ module TrailGuide
76
77
  end
77
78
 
78
79
  def started?
79
- !!started_at
80
+ started_at && started_at <= Time.now
80
81
  end
81
82
 
82
83
  def stopped?
83
- !!stopped_at
84
+ stopped_at && stopped_at <= Time.now
84
85
  end
85
86
 
86
87
  def running?
@@ -162,7 +163,7 @@ module TrailGuide
162
163
  :allow_multiple_goals?, :track_winner_conversions?, :callbacks, to: :class
163
164
 
164
165
  def initialize(participant)
165
- @participant = participant
166
+ @participant = TrailGuide::Experiments::Participant.new(self, participant)
166
167
  end
167
168
 
168
169
  def algorithm
@@ -177,37 +178,49 @@ module TrailGuide
177
178
  return control if TrailGuide.configuration.disabled
178
179
 
179
180
  variant = choose_variant!(override: override, metadata: metadata, **opts)
180
- participant.participating!(variant) unless override.present? && !configuration.store_override
181
181
  run_callbacks(:on_use, variant, metadata)
182
182
  variant
183
183
  rescue Errno::ECONNREFUSED, Redis::BaseError, SocketError => e
184
184
  run_callbacks(:on_redis_failover, e)
185
- return variants.find { |var| var == override } if override.present?
185
+ return variants.find { |var| var == override } || control if override.present?
186
186
  return control
187
187
  end
188
188
 
189
189
  def choose_variant!(override: nil, excluded: false, metadata: nil)
190
190
  return control if TrailGuide.configuration.disabled
191
+
191
192
  if override.present?
192
- variant = variants.find { |var| var == override }
193
- return variant unless configuration.track_override && running?
194
- else
195
- if winner?
196
- variant = winner
197
- return variant unless track_winner_conversions? && running?
198
- else
199
- return control if excluded
200
- return control if !started? && configuration.start_manually
201
- start! unless started?
202
- return control unless running?
203
- return variants.find { |var| var == participant[storage_key] } if participating?
204
- return control unless TrailGuide.configuration.allow_multiple_experiments == true || !participant.participating_in_active_experiments?(TrailGuide.configuration.allow_multiple_experiments == false)
205
-
206
- variant = algorithm_choose!(metadata: metadata)
193
+ variant = variants.find { |var| var == override } || control
194
+ if running?
195
+ variant.increment_participation! if configuration.track_override
196
+ participant.participating!(variant) if configuration.store_override
207
197
  end
198
+ return variant
199
+ end
200
+
201
+ if winner?
202
+ variant = winner
203
+ variant.increment_participation! if track_winner_conversions?
204
+ return variant
208
205
  end
209
206
 
207
+ return control if excluded
208
+ return control if !started? && configuration.start_manually
209
+ start! unless started?
210
+ return control unless running?
211
+
212
+ if participant.participating?
213
+ variant = participant.variant
214
+ participant.participating!(variant)
215
+ return variant
216
+ end
217
+
218
+ return control unless TrailGuide.configuration.allow_multiple_experiments == true || !participant.participating_in_active_experiments?(TrailGuide.configuration.allow_multiple_experiments == false)
219
+ return control unless allow_participation?(metadata)
220
+
221
+ variant = algorithm_choose!(metadata: metadata)
210
222
  variant.increment_participation!
223
+ participant.participating!(variant)
211
224
  run_callbacks(:on_choose, variant, metadata)
212
225
  variant
213
226
  end
@@ -218,17 +231,18 @@ module TrailGuide
218
231
 
219
232
  def convert!(checkpoint=nil, metadata: nil)
220
233
  return false if !running? || (winner? && !track_winner_conversions?)
221
- return false unless participating?
234
+ return false unless participant.participating?
222
235
  raise InvalidGoalError, "Invalid goal checkpoint `#{checkpoint}` for `#{experiment_name}`." unless checkpoint.present? || goals.empty?
223
236
  raise InvalidGoalError, "Invalid goal checkpoint `#{checkpoint}` for `#{experiment_name}`." unless checkpoint.nil? || goals.any? { |goal| goal == checkpoint.to_s.underscore.to_sym }
224
237
  # TODO eventually allow progressing through funnel checkpoints towards goals
225
- if converted?(checkpoint)
238
+ if participant.converted?(checkpoint)
226
239
  return false unless allow_multiple_conversions?
227
- elsif converted?
240
+ elsif participant.converted?
228
241
  return false unless allow_multiple_goals?
229
242
  end
243
+ return false unless allow_conversion?(checkpoint, metadata)
230
244
 
231
- variant = variants.find { |var| var == participant[storage_key] }
245
+ variant = participant.variant
232
246
  # TODO eventually only reset if we're at the final goal in a funnel
233
247
  participant.converted!(variant, checkpoint, reset: !reset_manually?)
234
248
  variant.increment_conversion!(checkpoint)
@@ -239,22 +253,24 @@ module TrailGuide
239
253
  return false
240
254
  end
241
255
 
242
- def participating?
243
- participant.participating?(self)
256
+ def allow_participation?(metadata=nil)
257
+ return true if callbacks[:allow_participation].empty?
258
+ run_callbacks(:allow_participation, metadata)
244
259
  end
245
260
 
246
- def converted?(checkpoint=nil)
247
- participant.converted?(self, checkpoint)
261
+ def allow_conversion?(checkpoint=nil, metadata=nil)
262
+ return true if callbacks[:allow_conversion].empty?
263
+ run_callbacks(:allow_conversion, checkpoint, metadata)
248
264
  end
249
265
 
250
266
  def run_callbacks(hook, *args)
251
267
  return unless callbacks[hook]
252
- if hook == :rollout_winner
253
- callbacks[hook].reduce(args[0]) do |winner, callback|
268
+ if [:allow_participation, :allow_conversion, :rollout_winner].include?(hook)
269
+ callbacks[hook].reduce(args.slice!(0,1)[0]) do |result, callback|
254
270
  if callback.respond_to?(:call)
255
- callback.call(self, winner)
271
+ callback.call(self, result, *args)
256
272
  else
257
- send(callback, self, winner)
273
+ send(callback, self, result, *args)
258
274
  end
259
275
  end
260
276
  else
@@ -12,7 +12,7 @@ module TrailGuide
12
12
  :on_start, :on_stop, :on_resume, :on_winner, :on_reset, :on_delete,
13
13
  :on_choose, :on_use, :on_convert,
14
14
  :on_redis_failover,
15
- :rollout_winner
15
+ :allow_participation, :allow_conversion, :rollout_winner
16
16
  ].freeze
17
17
 
18
18
  def default_config
@@ -185,6 +185,16 @@ module TrailGuide
185
185
  self[:on_redis_failover] << (meth || block)
186
186
  end
187
187
 
188
+ def allow_participation(meth=nil, &block)
189
+ self[:allow_participation] ||= []
190
+ self[:allow_participation] << (meth || block)
191
+ end
192
+
193
+ def allow_conversion(meth=nil, &block)
194
+ self[:allow_conversion] ||= []
195
+ self[:allow_conversion] << (meth || block)
196
+ end
197
+
188
198
  def rollout_winner(meth=nil, &block)
189
199
  self[:rollout_winner] ||= []
190
200
  self[:rollout_winner] << (meth || block)
@@ -0,0 +1,33 @@
1
+ module TrailGuide
2
+ module Experiments
3
+ class Participant
4
+ attr_reader :experiment, :participant
5
+
6
+ def initialize(experiment, participant)
7
+ @experiment = experiment
8
+ @participant = participant
9
+ end
10
+
11
+ def participating?
12
+ participant.participating?(experiment)
13
+ end
14
+
15
+ def converted?(checkpoint=nil)
16
+ participant.converted?(experiment, checkpoint)
17
+ end
18
+
19
+ def variant
20
+ participant.variant(experiment)
21
+ end
22
+
23
+ def method_missing(meth, *args, &block)
24
+ return participant.send(meth, *args, &block) if participant.respond_to?(meth, true)
25
+ super
26
+ end
27
+
28
+ def respond_to_missing?(meth, include_private=false)
29
+ participant.respond_to?(meth, include_private)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -6,6 +6,7 @@ module TrailGuide
6
6
  def initialize(context, adapter: nil)
7
7
  @context = context
8
8
  @adapter = adapter.new(context) unless adapter.nil?
9
+ cleanup_inactive_experiments!
9
10
  end
10
11
 
11
12
  def adapter
@@ -128,5 +129,19 @@ module TrailGuide
128
129
  experiment && !experiment.combined? && experiment.running? && participating?(experiment, include_control)
129
130
  end
130
131
  end
132
+
133
+ def cleanup_inactive_experiments!
134
+ return false if adapter.keys.empty?
135
+
136
+ adapter.keys.each do |key|
137
+ experiment_name = key.to_s.split(":").first.to_sym
138
+ experiment = TrailGuide.catalog.find(experiment_name)
139
+ if !experiment || !experiment.started?
140
+ adapter.delete(key)
141
+ end
142
+ end
143
+
144
+ return true
145
+ end
131
146
  end
132
147
  end
@@ -1,15 +1,31 @@
1
1
  module TrailGuide
2
2
  module SpecHelper
3
3
 
4
- def use_trailguide(**experiments)
5
- experiments.each do |exp,var|
6
- experiment = TrailGuide.catalog.find(exp)
7
- raise ArgumentError, "Experiment not found `#{exp}`" unless experiment.present?
8
- variant = experiment.variants.find { |v| v == var }
9
- raise ArgumentError, "Variant `#{var}` not found in experiment `#{exp}`" unless variant.present?
10
-
11
- allow_any_instance_of(experiment).to receive(:choose!).and_return(variant)
4
+ def use_trailguide(**experiments, &block)
5
+ if block_given?
6
+ before do
7
+ use_trailguide(**experiments)
8
+ end
9
+
10
+ yield
11
+ else
12
+ experiments.each do |exp,var|
13
+ experiment = TrailGuide.catalog.find(exp)
14
+ raise ArgumentError, "Experiment not found `#{exp}`" unless experiment.present?
15
+ variant = experiment.variants.find { |v| v == var }
16
+ raise ArgumentError, "Variant `#{var}` not found in experiment `#{exp}`" unless variant.present?
17
+
18
+ allow_any_instance_of(experiment).to receive(:choose!).and_return(variant)
19
+ end
12
20
  end
13
21
  end
22
+ alias_method :with_trailguide, :use_trailguide
23
+ end
24
+ end
25
+
26
+ if defined?(RSpec)
27
+ RSpec.configure do |config|
28
+ config.extend TrailGuide::SpecHelper
29
+ config.include TrailGuide::SpecHelper
14
30
  end
15
31
  end
@@ -53,7 +53,8 @@ module TrailGuide
53
53
  end
54
54
 
55
55
  def reset!
56
- delete! && save!
56
+ delete!
57
+ save!
57
58
  end
58
59
 
59
60
  def participants
@@ -2,7 +2,7 @@ module TrailGuide
2
2
  module Version
3
3
  MAJOR = 0
4
4
  MINOR = 1
5
- PATCH = 19
5
+ PATCH = 20
6
6
  VERSION = "#{MAJOR}.#{MINOR}.#{PATCH}"
7
7
 
8
8
  class << self
@@ -1,5 +1,6 @@
1
1
  require "canfig"
2
2
  require "redis"
3
+ require "trail_guide/version"
3
4
  require "trail_guide/config"
4
5
  require "trail_guide/errors"
5
6
  require "trail_guide/adapters"
@@ -11,7 +12,7 @@ require "trail_guide/combined_experiment"
11
12
  require "trail_guide/catalog"
12
13
  require "trail_guide/helper"
13
14
  require "trail_guide/engine"
14
- require "trail_guide/version"
15
+ require "trail_guide/admin"
15
16
 
16
17
  module TrailGuide
17
18
  include Canfig::Module
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: trailguide
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.19
4
+ version: 0.1.20
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mark Rebec
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-03-11 00:00:00.000000000 Z
11
+ date: 2019-03-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -159,6 +159,7 @@ files:
159
159
  - lib/trail_guide/experiments/base.rb
160
160
  - lib/trail_guide/experiments/combined_config.rb
161
161
  - lib/trail_guide/experiments/config.rb
162
+ - lib/trail_guide/experiments/participant.rb
162
163
  - lib/trail_guide/helper.rb
163
164
  - lib/trail_guide/participant.rb
164
165
  - lib/trail_guide/spec_helper.rb