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