trailguide 0.2.1 → 0.3.0
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/README.md +191 -293
- data/app/views/trail_guide/admin/experiments/_btn_join.html.erb +1 -1
- data/app/views/trail_guide/admin/experiments/_header.html.erb +3 -3
- data/config/initializers/admin.rb +19 -0
- data/config/initializers/experiment.rb +261 -0
- data/config/initializers/trailguide.rb +6 -279
- data/lib/trail_guide/adapters.rb +2 -0
- data/lib/trail_guide/adapters/experiments.rb +8 -0
- data/lib/trail_guide/adapters/experiments/redis.rb +48 -0
- data/lib/trail_guide/adapters/participants/cookie.rb +1 -0
- data/lib/trail_guide/adapters/participants/unity.rb +9 -1
- data/lib/trail_guide/adapters/variants.rb +8 -0
- data/lib/trail_guide/adapters/variants/redis.rb +52 -0
- data/lib/trail_guide/admin/engine.rb +1 -0
- data/lib/trail_guide/algorithms.rb +4 -0
- data/lib/trail_guide/algorithms/algorithm.rb +29 -0
- data/lib/trail_guide/algorithms/bandit.rb +9 -18
- data/lib/trail_guide/algorithms/distributed.rb +8 -15
- data/lib/trail_guide/algorithms/random.rb +2 -12
- data/lib/trail_guide/algorithms/static.rb +34 -0
- data/lib/trail_guide/algorithms/weighted.rb +5 -17
- data/lib/trail_guide/catalog.rb +79 -35
- data/lib/trail_guide/config.rb +2 -4
- data/lib/trail_guide/engine.rb +2 -1
- data/lib/trail_guide/experiments/base.rb +41 -24
- data/lib/trail_guide/experiments/combined_config.rb +4 -0
- data/lib/trail_guide/experiments/config.rb +59 -30
- data/lib/trail_guide/experiments/participant.rb +4 -2
- data/lib/trail_guide/helper.rb +4 -216
- data/lib/trail_guide/helper/experiment_proxy.rb +160 -0
- data/lib/trail_guide/helper/helper_proxy.rb +62 -0
- data/lib/trail_guide/metrics/config.rb +2 -0
- data/lib/trail_guide/metrics/goal.rb +17 -15
- data/lib/trail_guide/participant.rb +10 -2
- data/lib/trail_guide/unity.rb +17 -8
- data/lib/trail_guide/variant.rb +15 -11
- data/lib/trail_guide/version.rb +2 -2
- metadata +13 -3
data/lib/trail_guide/adapters.rb
CHANGED
@@ -0,0 +1,48 @@
|
|
1
|
+
module TrailGuide
|
2
|
+
module Adapters
|
3
|
+
module Experiments
|
4
|
+
class Redis
|
5
|
+
attr_reader :experiment
|
6
|
+
delegate :storage_key, to: :experiment
|
7
|
+
|
8
|
+
def initialize(experiment, redis: nil)
|
9
|
+
@experiment = experiment
|
10
|
+
@redis = redis
|
11
|
+
end
|
12
|
+
|
13
|
+
def redis
|
14
|
+
@redis ||= TrailGuide.redis
|
15
|
+
end
|
16
|
+
|
17
|
+
def get(attr)
|
18
|
+
redis.hget(storage_key, attr.to_s)
|
19
|
+
end
|
20
|
+
|
21
|
+
def set(attr, val)
|
22
|
+
redis.hset(storage_key, attr.to_s, val.to_s)
|
23
|
+
val.to_s
|
24
|
+
end
|
25
|
+
|
26
|
+
def setnx(attr, val)
|
27
|
+
val.to_s if redis.hsetnx(storage_key, attr.to_s, val.to_s)
|
28
|
+
end
|
29
|
+
|
30
|
+
def delete(attr)
|
31
|
+
redis.hdel(storage_key, attr.to_s)
|
32
|
+
end
|
33
|
+
|
34
|
+
def exists?(attr)
|
35
|
+
redis.hexists(storage_key, attr.to_s)
|
36
|
+
end
|
37
|
+
|
38
|
+
def persisted?
|
39
|
+
redis.exists(storage_key)
|
40
|
+
end
|
41
|
+
|
42
|
+
def destroy
|
43
|
+
redis.del(storage_key)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -8,6 +8,10 @@ module TrailGuide
|
|
8
8
|
|
9
9
|
def initialize(&block)
|
10
10
|
configure do |config|
|
11
|
+
# TODO make visitor/user configuration more flexible, allow stuff like:
|
12
|
+
#
|
13
|
+
# config.visitor = -> (context) { context.cookies['visitor_id'] }
|
14
|
+
# config.user = -> (context) { context.current_user.try(:id) }
|
11
15
|
config.visitor_cookie = nil
|
12
16
|
config.user_id_key = :id
|
13
17
|
|
@@ -17,6 +21,7 @@ module TrailGuide
|
|
17
21
|
config.expiration = 1.year.seconds
|
18
22
|
end
|
19
23
|
|
24
|
+
# TODO use cookie or session adapter by default instead?
|
20
25
|
config.visitor_adapter = TrailGuide::Adapters::Participants::Redis.configure do |config|
|
21
26
|
config.namespace = 'unity:visitors'
|
22
27
|
config.lookup = -> (visitor_id) { visitor_id }
|
@@ -43,7 +48,7 @@ module TrailGuide
|
|
43
48
|
end
|
44
49
|
|
45
50
|
if logged_out_context?
|
46
|
-
unity.visitor_id ||= context.send(:cookies)[configuration.visitor_cookie]
|
51
|
+
unity.visitor_id ||= context.send(:cookies)[configuration.visitor_cookie]
|
47
52
|
end
|
48
53
|
|
49
54
|
unity.sync!
|
@@ -57,9 +62,12 @@ module TrailGuide
|
|
57
62
|
end
|
58
63
|
end
|
59
64
|
|
65
|
+
# TODO introduce Unity::Adapter class, which wraps BOTH/ALL of the configured adapters (either anonymous, visitor, user, or visitor+user) to keep everything in sync between them
|
66
|
+
|
60
67
|
protected
|
61
68
|
|
62
69
|
def context_type
|
70
|
+
# TODO allow a configuration.preference when both visitor and user context are available?
|
63
71
|
if visitor_context?
|
64
72
|
return :visitor
|
65
73
|
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module TrailGuide
|
2
|
+
module Adapters
|
3
|
+
module Variants
|
4
|
+
class Redis
|
5
|
+
attr_reader :variant
|
6
|
+
delegate :storage_key, to: :variant
|
7
|
+
|
8
|
+
def initialize(variant, redis: nil)
|
9
|
+
@variant = variant
|
10
|
+
@redis = redis
|
11
|
+
end
|
12
|
+
|
13
|
+
def redis
|
14
|
+
@redis ||= TrailGuide.redis
|
15
|
+
end
|
16
|
+
|
17
|
+
def get(attr)
|
18
|
+
redis.hget(storage_key, attr.to_s)
|
19
|
+
end
|
20
|
+
|
21
|
+
def set(attr, val)
|
22
|
+
redis.hset(storage_key, attr.to_s, val.to_s)
|
23
|
+
val.to_s
|
24
|
+
end
|
25
|
+
|
26
|
+
def setnx(attr, val)
|
27
|
+
val.to_s if redis.hsetnx(storage_key, attr.to_s, val.to_s)
|
28
|
+
end
|
29
|
+
|
30
|
+
def increment(attr, cnt=1)
|
31
|
+
redis.hincrby(storage_key, attr.to_s, cnt)
|
32
|
+
end
|
33
|
+
|
34
|
+
def delete(attr)
|
35
|
+
redis.hdel(storage_key, attr.to_s)
|
36
|
+
end
|
37
|
+
|
38
|
+
def exists?(attr)
|
39
|
+
redis.hexists(storage_key, attr.to_s)
|
40
|
+
end
|
41
|
+
|
42
|
+
def persisted?
|
43
|
+
redis.exists(storage_key)
|
44
|
+
end
|
45
|
+
|
46
|
+
def destroy
|
47
|
+
redis.del(storage_key)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -1,7 +1,9 @@
|
|
1
|
+
require "trail_guide/algorithms/algorithm"
|
1
2
|
require "trail_guide/algorithms/weighted"
|
2
3
|
require "trail_guide/algorithms/distributed"
|
3
4
|
require "trail_guide/algorithms/bandit"
|
4
5
|
require "trail_guide/algorithms/random"
|
6
|
+
require "trail_guide/algorithms/static"
|
5
7
|
|
6
8
|
module TrailGuide
|
7
9
|
module Algorithms
|
@@ -15,6 +17,8 @@ module TrailGuide
|
|
15
17
|
algo = TrailGuide::Algorithms::Distributed
|
16
18
|
when :random
|
17
19
|
algo = TrailGuide::Algorithms::Random
|
20
|
+
when :static
|
21
|
+
algo = TrailGuide::Algorithms::Static
|
18
22
|
else
|
19
23
|
algo = algo.constantize if algo.is_a?(String)
|
20
24
|
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module TrailGuide
|
2
|
+
module Algorithms
|
3
|
+
class Algorithm
|
4
|
+
attr_reader :experiment
|
5
|
+
|
6
|
+
def self.choose!(experiment, **opts)
|
7
|
+
new(experiment).choose!(**opts)
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(experiment)
|
11
|
+
@experiment = experiment
|
12
|
+
end
|
13
|
+
|
14
|
+
def choose!(**opts)
|
15
|
+
raise NotImplementedError, 'You must define a `#choose!(**opts)` method for your algorithm'
|
16
|
+
end
|
17
|
+
|
18
|
+
protected
|
19
|
+
|
20
|
+
def control
|
21
|
+
experiment.control
|
22
|
+
end
|
23
|
+
|
24
|
+
def variants
|
25
|
+
experiment.variants
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -2,32 +2,23 @@ require 'simple-random'
|
|
2
2
|
|
3
3
|
module TrailGuide
|
4
4
|
module Algorithms
|
5
|
-
class Bandit
|
6
|
-
attr_reader :experiment
|
7
|
-
|
8
|
-
def self.choose!(experiment, **opts)
|
9
|
-
new(experiment).choose!(**opts)
|
10
|
-
end
|
11
|
-
|
12
|
-
def initialize(experiment)
|
13
|
-
@experiment = experiment
|
14
|
-
end
|
15
|
-
|
5
|
+
class Bandit < Algorithm
|
16
6
|
def choose!(**opts)
|
17
|
-
|
18
|
-
experiment.variants.find { |var| var == guess }
|
7
|
+
variants.find { |var| var == best_guess }
|
19
8
|
end
|
20
9
|
|
21
10
|
private
|
22
11
|
|
12
|
+
def guesses
|
13
|
+
@guesses ||= variants.map do |variant|
|
14
|
+
[variant.name, arm_guess(variant.participants, variant.converted)]
|
15
|
+
end.to_h
|
16
|
+
end
|
17
|
+
|
23
18
|
def best_guess
|
24
19
|
@best_guess ||= begin
|
25
|
-
guesses = {}
|
26
|
-
experiment.variants.each do |variant|
|
27
|
-
guesses[variant.name] = arm_guess(variant.participants, variant.converted)
|
28
|
-
end
|
29
20
|
gmax = guesses.values.max
|
30
|
-
best = guesses.keys.select { |name| guesses[name] ==
|
21
|
+
best = guesses.keys.select { |name| guesses[name] == gmax }
|
31
22
|
best.sample
|
32
23
|
end
|
33
24
|
end
|
@@ -1,25 +1,18 @@
|
|
1
1
|
module TrailGuide
|
2
2
|
module Algorithms
|
3
|
-
class Distributed
|
4
|
-
attr_reader :experiment
|
5
|
-
|
6
|
-
def self.choose!(experiment, **opts)
|
7
|
-
new(experiment).choose!(**opts)
|
8
|
-
end
|
9
|
-
|
10
|
-
def initialize(experiment)
|
11
|
-
@experiment = experiment
|
12
|
-
end
|
13
|
-
|
3
|
+
class Distributed < Algorithm
|
14
4
|
def choose!(**opts)
|
15
|
-
|
5
|
+
options.sample
|
16
6
|
end
|
17
7
|
|
18
8
|
private
|
19
9
|
|
20
|
-
def
|
21
|
-
|
22
|
-
|
10
|
+
def grouped
|
11
|
+
@grouped ||= variants.group_by(&:participants)
|
12
|
+
end
|
13
|
+
|
14
|
+
def options
|
15
|
+
@options ||= grouped.min_by { |c,g| c }.last
|
23
16
|
end
|
24
17
|
end
|
25
18
|
end
|
@@ -1,18 +1,8 @@
|
|
1
1
|
module TrailGuide
|
2
2
|
module Algorithms
|
3
|
-
class Random
|
4
|
-
attr_reader :experiment
|
5
|
-
|
6
|
-
def self.choose!(experiment, **opts)
|
7
|
-
new(experiment).choose!(**opts)
|
8
|
-
end
|
9
|
-
|
10
|
-
def initialize(experiment)
|
11
|
-
@experiment = experiment
|
12
|
-
end
|
13
|
-
|
3
|
+
class Random < Algorithm
|
14
4
|
def choose!(**opts)
|
15
|
-
|
5
|
+
variants.sample
|
16
6
|
end
|
17
7
|
end
|
18
8
|
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module TrailGuide
|
2
|
+
module Algorithms
|
3
|
+
class Static < Algorithm
|
4
|
+
def self.choose!(experiment, metadata: nil, &block)
|
5
|
+
new(experiment, &block).choose!(metadata: metadata)
|
6
|
+
end
|
7
|
+
|
8
|
+
def initialize(experiment=nil, &block)
|
9
|
+
raise ArgumentError, 'You must provide a comparison block when using the static algorithm' unless block_given?
|
10
|
+
@block = block
|
11
|
+
super(experiment)
|
12
|
+
end
|
13
|
+
|
14
|
+
def new(experiment)
|
15
|
+
TrailGuide.logger.warn "WARNING: Using the Static algorithm for an experiment which is configured with sticky_assignment. You should either use a different algorithm or configure sticky_assignment for the `#{experiment.experiment_name}` experiment." if experiment.configuration.sticky_assignment?
|
16
|
+
self.class.new(experiment, &@block)
|
17
|
+
end
|
18
|
+
|
19
|
+
def choose!(metadata: nil)
|
20
|
+
return control unless metadata.present?
|
21
|
+
|
22
|
+
variant = variants.find do |variant|
|
23
|
+
@block.call(variant.metadata, metadata)
|
24
|
+
end
|
25
|
+
|
26
|
+
variant || control
|
27
|
+
rescue => e
|
28
|
+
TrailGuide.logger.error "#{e.class.name}: #{e.message}"
|
29
|
+
TrailGuide.logger.error e.backtrace.first
|
30
|
+
control
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -1,23 +1,11 @@
|
|
1
1
|
module TrailGuide
|
2
2
|
module Algorithms
|
3
|
-
class Weighted
|
4
|
-
attr_reader :experiment
|
5
|
-
|
6
|
-
def self.choose!(experiment, **opts)
|
7
|
-
new(experiment).choose!(**opts)
|
8
|
-
end
|
9
|
-
|
10
|
-
def initialize(experiment)
|
11
|
-
@experiment = experiment
|
12
|
-
end
|
13
|
-
|
3
|
+
class Weighted < Algorithm
|
14
4
|
def choose!(**opts)
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
return variant if weight >= reference
|
20
|
-
reference -= weight
|
5
|
+
reference = rand * variants.sum(&:weight)
|
6
|
+
variants.each do |variant|
|
7
|
+
return variant if variant.weight >= reference
|
8
|
+
reference -= variant.weight
|
21
9
|
end
|
22
10
|
end
|
23
11
|
end
|
data/lib/trail_guide/catalog.rb
CHANGED
@@ -7,19 +7,23 @@ module TrailGuide
|
|
7
7
|
@catalog ||= new
|
8
8
|
end
|
9
9
|
|
10
|
-
def load_experiments!
|
10
|
+
def load_experiments!(configs: [], classes: [])
|
11
11
|
@catalog = nil
|
12
12
|
|
13
13
|
# Load experiments from YAML configs if any exists
|
14
|
-
|
15
|
-
|
14
|
+
[configs].flatten.each do |path|
|
15
|
+
Dir[Rails.root.join(path)].each { |f| load_yaml_experiments(f) if ['.yml', '.yaml'].include?(File.extname(f)) }
|
16
|
+
end
|
16
17
|
|
17
18
|
# Load experiments from ruby configs if any exist
|
18
|
-
|
19
|
-
|
19
|
+
[configs].flatten.each do |path|
|
20
|
+
Dir[Rails.root.join(path)].each { |f| DSL.instance_eval(File.read(f)) if File.extname(f) == '.rb' }
|
21
|
+
end
|
20
22
|
|
21
23
|
# Load any experiment classes defined in the app
|
22
|
-
|
24
|
+
[classes].flatten.each do |path|
|
25
|
+
Dir[Rails.root.join(path)].each { |f| load f }
|
26
|
+
end
|
23
27
|
end
|
24
28
|
|
25
29
|
def load_yaml_experiments(file)
|
@@ -46,7 +50,7 @@ module TrailGuide
|
|
46
50
|
|
47
51
|
expgoals.each do |expgoal|
|
48
52
|
goal expgoal
|
49
|
-
end
|
53
|
+
end if expgoals.present?
|
50
54
|
|
51
55
|
config.control = options[:control] if options[:control]
|
52
56
|
config.groups = options[:groups] if options[:groups]
|
@@ -76,15 +80,24 @@ module TrailGuide
|
|
76
80
|
end
|
77
81
|
end
|
78
82
|
|
79
|
-
|
80
|
-
|
83
|
+
attr_reader :experiments, :combined
|
84
|
+
delegate :new, to: :class
|
85
|
+
delegate :each, to: :experiments
|
81
86
|
|
82
|
-
def initialize(experiments=[])
|
87
|
+
def initialize(experiments=[], combined=[])
|
83
88
|
@experiments = experiments
|
89
|
+
@combined = combined
|
84
90
|
end
|
85
91
|
|
86
|
-
def
|
87
|
-
|
92
|
+
def combined_experiment(exp, name)
|
93
|
+
combo = @combined.find do |cex|
|
94
|
+
cex.experiment_name == name.to_s.underscore.to_sym &&
|
95
|
+
cex.parent.experiment_name == exp.experiment_name
|
96
|
+
end
|
97
|
+
return combo if combo.present?
|
98
|
+
combo = self.class.combined_experiment(exp, name)
|
99
|
+
@combined << combo
|
100
|
+
combo
|
88
101
|
end
|
89
102
|
|
90
103
|
def groups
|
@@ -100,47 +113,49 @@ module TrailGuide
|
|
100
113
|
end
|
101
114
|
end.flatten
|
102
115
|
|
103
|
-
|
116
|
+
new(exploded, @combined)
|
104
117
|
end
|
105
118
|
|
106
119
|
def calibrating
|
107
|
-
|
120
|
+
new(to_a.select(&:calibrating?), @combined)
|
108
121
|
end
|
109
122
|
|
110
123
|
def started
|
111
|
-
|
124
|
+
new(to_a.select { |e| e.started? && !e.winner? }, @combined)
|
112
125
|
end
|
113
126
|
|
114
127
|
def scheduled
|
115
|
-
|
128
|
+
new(to_a.select { |e| e.scheduled? && !e.winner? }, @combined)
|
116
129
|
end
|
117
130
|
|
118
131
|
def running
|
119
|
-
|
132
|
+
new(to_a.select { |e| e.running? && !e.winner? }, @combined)
|
120
133
|
end
|
121
134
|
|
122
135
|
def paused
|
123
|
-
|
136
|
+
new(to_a.select { |e| e.paused? && !e.winner? }, @combined)
|
124
137
|
end
|
125
138
|
|
126
139
|
def stopped
|
127
|
-
|
140
|
+
new(to_a.select { |e| e.stopped? && !e.winner? }, @combined)
|
128
141
|
end
|
129
142
|
|
130
143
|
def ended
|
131
|
-
|
144
|
+
new(to_a.select(&:winner?), @combined)
|
132
145
|
end
|
133
146
|
|
134
147
|
def unstarted
|
135
|
-
|
148
|
+
new(to_a.select { |e| !e.started? && !e.calibrating? && !e.scheduled? && !e.winner? }, @combined)
|
136
149
|
end
|
137
150
|
|
138
151
|
def not_running
|
139
|
-
|
152
|
+
new(to_a.select { |e| !e.running? }, @combined)
|
140
153
|
end
|
141
154
|
|
142
155
|
def by_started
|
143
156
|
scoped = to_a.sort do |a,b|
|
157
|
+
# TODO finish implementing specs, then implement `experiment.fresh?`, then (maybe) re-work this all
|
158
|
+
# into an experiment spaceship operator
|
144
159
|
if !(a.started? || a.scheduled? || a.winner?) && !(b.started? || b.scheduled? || b.winner?)
|
145
160
|
a.experiment_name.to_s <=> b.experiment_name.to_s
|
146
161
|
elsif !(a.started? || a.scheduled? || a.winner?)
|
@@ -159,32 +174,48 @@ module TrailGuide
|
|
159
174
|
elsif !a.running? && b.running?
|
160
175
|
1
|
161
176
|
elsif a.running? && b.running?
|
162
|
-
a.started_at
|
177
|
+
if a.started_at == b.started_at
|
178
|
+
a.experiment_name.to_s <=> b.experiment_name.to_s
|
179
|
+
else
|
180
|
+
a.started_at <=> b.started_at
|
181
|
+
end
|
163
182
|
elsif a.paused? && !b.paused?
|
164
183
|
-1
|
165
184
|
elsif !a.paused? && b.paused?
|
166
185
|
1
|
167
186
|
elsif a.paused? && b.paused?
|
168
|
-
a.paused_at
|
187
|
+
if a.paused_at == b.paused_at
|
188
|
+
a.experiment_name.to_s <=> b.experiment_name.to_s
|
189
|
+
else
|
190
|
+
a.paused_at <=> b.paused_at
|
191
|
+
end
|
169
192
|
elsif a.scheduled? && !b.scheduled?
|
170
193
|
-1
|
171
194
|
elsif !a.scheduled? && b.scheduled?
|
172
195
|
1
|
173
196
|
elsif a.scheduled? && b.scheduled?
|
174
|
-
a.started_at
|
197
|
+
if a.started_at == b.started_at
|
198
|
+
a.experiment_name.to_s <=> b.experiment_name.to_s
|
199
|
+
else
|
200
|
+
a.started_at <=> b.started_at
|
201
|
+
end
|
175
202
|
elsif a.stopped? && !b.stopped?
|
176
|
-
-1
|
203
|
+
-1 # TODO remove unused case
|
177
204
|
elsif !a.stopped? && b.stopped?
|
178
|
-
1
|
205
|
+
1 # TODO remove unused case
|
179
206
|
elsif a.stopped? && b.stopped?
|
180
|
-
a.stopped_at
|
207
|
+
if a.stopped_at == b.stopped_at
|
208
|
+
a.experiment_name.to_s <=> b.experiment_name.to_s
|
209
|
+
else
|
210
|
+
a.stopped_at <=> b.stopped_at
|
211
|
+
end
|
181
212
|
else
|
182
213
|
a.experiment_name.to_s <=> b.experiment_name.to_s
|
183
214
|
end
|
184
215
|
end
|
185
216
|
end
|
186
217
|
|
187
|
-
|
218
|
+
new(scoped, @combined)
|
188
219
|
end
|
189
220
|
|
190
221
|
def find(name)
|
@@ -227,7 +258,7 @@ module TrailGuide
|
|
227
258
|
end
|
228
259
|
end
|
229
260
|
|
230
|
-
|
261
|
+
new(selected, @combined)
|
231
262
|
end
|
232
263
|
|
233
264
|
def register(klass)
|
@@ -235,8 +266,13 @@ module TrailGuide
|
|
235
266
|
klass
|
236
267
|
end
|
237
268
|
|
238
|
-
def deregister(key)
|
239
|
-
|
269
|
+
def deregister(key, remove_const=false)
|
270
|
+
klass = find(key)
|
271
|
+
return unless klass.present?
|
272
|
+
experiments.delete(klass)
|
273
|
+
return klass unless remove_const && klass.name.present?
|
274
|
+
Object.send(:remove_const, :"#{klass.name}")
|
275
|
+
return key
|
240
276
|
end
|
241
277
|
|
242
278
|
def export
|
@@ -256,9 +292,9 @@ module TrailGuide
|
|
256
292
|
|
257
293
|
experiment.reset!
|
258
294
|
TrailGuide.redis.hsetnx(experiment.storage_key, 'name', experiment.experiment_name)
|
259
|
-
TrailGuide.redis.hset(experiment.storage_key, 'started_at', est['started_at']) if est['started_at'].present?
|
260
|
-
TrailGuide.redis.hset(experiment.storage_key, 'paused_at', est['paused_at']) if est['paused_at'].present?
|
261
|
-
TrailGuide.redis.hset(experiment.storage_key, 'stopped_at', est['stopped_at']) if est['stopped_at'].present?
|
295
|
+
TrailGuide.redis.hset(experiment.storage_key, 'started_at', DateTime.parse(est['started_at']).to_i) if est['started_at'].present?
|
296
|
+
TrailGuide.redis.hset(experiment.storage_key, 'paused_at', DateTime.parse(est['paused_at']).to_i) if est['paused_at'].present?
|
297
|
+
TrailGuide.redis.hset(experiment.storage_key, 'stopped_at', DateTime.parse(est['stopped_at']).to_i) if est['stopped_at'].present?
|
262
298
|
TrailGuide.redis.hset(experiment.storage_key, 'winner', est['winner']) if est['winner'].present?
|
263
299
|
|
264
300
|
est['variants'].each do |var,vst|
|
@@ -277,6 +313,13 @@ module TrailGuide
|
|
277
313
|
end
|
278
314
|
end
|
279
315
|
|
316
|
+
def missing
|
317
|
+
TrailGuide.redis.keys.select do |key|
|
318
|
+
exp = key.split(':').first
|
319
|
+
find(exp).nil?
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
280
323
|
def orphaned(key, trace)
|
281
324
|
added = TrailGuide.redis.sadd("orphans:#{key}", trace)
|
282
325
|
TrailGuide.redis.expire("orphans:#{key}", 15.minutes.seconds)
|
@@ -319,6 +362,7 @@ module TrailGuide
|
|
319
362
|
end
|
320
363
|
end
|
321
364
|
|
365
|
+
# TrailGuide.catalog
|
322
366
|
def self.catalog
|
323
367
|
TrailGuide::Catalog.catalog
|
324
368
|
end
|