trailguide 0.1.19 → 0.1.20

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