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 +4 -4
- data/config/initializers/trailguide.rb +28 -1
- data/config/routes.rb +12 -14
- data/lib/trail_guide/admin.rb +0 -5
- data/lib/trail_guide/catalog.rb +2 -3
- data/lib/trail_guide/experiments/base.rb +48 -32
- data/lib/trail_guide/experiments/config.rb +11 -1
- data/lib/trail_guide/experiments/participant.rb +33 -0
- data/lib/trail_guide/participant.rb +15 -0
- data/lib/trail_guide/spec_helper.rb +24 -8
- data/lib/trail_guide/variant.rb +2 -1
- data/lib/trail_guide/version.rb +1 -1
- data/lib/trailguide.rb +2 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c45506f04eec39f23d00fce31f84d1d1e24f4289e2870af413f8928d54cceef8
|
4
|
+
data.tar.gz: 1b6cb001e0fa544f6390e4a82e265922f9ab75b602f4406a2cddf337704fd076
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
data/config/routes.rb
CHANGED
@@ -12,22 +12,20 @@ TrailGuide::Engine.routes.draw do
|
|
12
12
|
via: [:put]
|
13
13
|
end
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
-
|
26
|
-
|
24
|
+
match :join, via: [:put, :post, :get], path: 'join/:variant'
|
25
|
+
match :leave, via: [:put, :post, :get]
|
27
26
|
|
28
|
-
|
29
|
-
|
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
|
data/lib/trail_guide/admin.rb
CHANGED
data/lib/trail_guide/catalog.rb
CHANGED
@@ -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
|
-
|
80
|
+
started_at && started_at <= Time.now
|
80
81
|
end
|
81
82
|
|
82
83
|
def stopped?
|
83
|
-
|
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
|
-
|
194
|
-
|
195
|
-
|
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 =
|
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
|
243
|
-
|
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
|
247
|
-
|
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
|
253
|
-
callbacks[hook].reduce(args[0]) do |
|
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,
|
271
|
+
callback.call(self, result, *args)
|
256
272
|
else
|
257
|
-
send(callback, self,
|
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
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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
|
data/lib/trail_guide/variant.rb
CHANGED
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/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/
|
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.
|
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
|
+
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
|