trailguide 0.2.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|