trailguide 0.1.31 → 0.2.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 +73 -8
- data/app/assets/javascripts/trail_guide/admin/application.js +20 -1
- data/app/assets/stylesheets/trail_guide/admin/application.css +22 -0
- data/app/assets/stylesheets/trail_guide/admin/experiments.css +36 -3
- data/app/controllers/trail_guide/admin/application_controller.rb +59 -8
- data/app/controllers/trail_guide/admin/experiments_controller.rb +209 -16
- data/app/controllers/trail_guide/admin/groups_controller.rb +34 -0
- data/app/controllers/trail_guide/experiments_controller.rb +1 -1
- data/app/views/layouts/trail_guide/admin/_calculator.erb +24 -0
- data/app/views/layouts/trail_guide/admin/_footer.html.erb +27 -0
- data/app/views/layouts/trail_guide/admin/_header.html.erb +147 -0
- data/app/views/layouts/trail_guide/admin/_import_modal.html.erb +45 -0
- data/app/views/layouts/trail_guide/admin/application.html.erb +17 -3
- data/app/views/trail_guide/admin/experiments/_alert_peek.html.erb +19 -0
- data/app/views/trail_guide/admin/experiments/_alert_state.html.erb +49 -0
- data/app/views/trail_guide/admin/experiments/_btn_analyze.html.erb +11 -0
- data/app/views/trail_guide/admin/experiments/_btn_analyze_goal.html.erb +5 -0
- data/app/views/trail_guide/admin/experiments/_btn_convert.html.erb +33 -0
- data/app/views/trail_guide/admin/experiments/_btn_enroll.html.erb +3 -0
- data/app/views/trail_guide/admin/experiments/_btn_join.html.erb +11 -0
- data/app/views/trail_guide/admin/experiments/_btn_leave.html.erb +7 -0
- data/app/views/trail_guide/admin/experiments/_btn_pause.html.erb +5 -0
- data/app/views/trail_guide/admin/experiments/_btn_peek.html.erb +13 -0
- data/app/views/trail_guide/admin/experiments/_btn_reset.html.erb +5 -0
- data/app/views/trail_guide/admin/experiments/_btn_restart.html.erb +5 -0
- data/app/views/trail_guide/admin/experiments/_btn_resume.html.erb +5 -0
- data/app/views/trail_guide/admin/experiments/_btn_schedule.html.erb +6 -0
- data/app/views/trail_guide/admin/experiments/_btn_start.html.erb +5 -0
- data/app/views/trail_guide/admin/experiments/_btn_stop.html.erb +5 -0
- data/app/views/trail_guide/admin/experiments/_experiment.html.erb +68 -172
- data/app/views/trail_guide/admin/experiments/_header.html.erb +87 -0
- data/app/views/trail_guide/admin/experiments/_start_modal.html.erb +57 -0
- data/app/views/trail_guide/admin/experiments/_tbody.html.erb +112 -0
- data/app/views/trail_guide/admin/experiments/_thead.html.erb +38 -0
- data/app/views/trail_guide/admin/experiments/index.html.erb +8 -16
- data/app/views/trail_guide/admin/experiments/show.html.erb +78 -0
- data/app/views/trail_guide/admin/groups/index.html.erb +40 -0
- data/app/views/trail_guide/admin/groups/show.html.erb +9 -0
- data/app/views/trail_guide/admin/orphans/_alert.html.erb +44 -0
- data/config/initializers/trailguide.rb +72 -9
- data/config/routes/admin.rb +19 -12
- data/lib/trail_guide/admin/engine.rb +2 -0
- data/lib/trail_guide/admin.rb +2 -0
- data/lib/trail_guide/calculators/bayesian.rb +98 -0
- data/lib/trail_guide/calculators/calculator.rb +58 -0
- data/lib/trail_guide/calculators/score.rb +68 -0
- data/lib/trail_guide/calculators.rb +8 -0
- data/lib/trail_guide/catalog.rb +134 -19
- data/lib/trail_guide/combined_experiment.rb +8 -3
- data/lib/trail_guide/config.rb +6 -1
- data/lib/trail_guide/engine.rb +2 -0
- data/lib/trail_guide/errors.rb +30 -1
- data/lib/trail_guide/experiment.rb +0 -1
- data/lib/trail_guide/experiments/base.rb +189 -53
- data/lib/trail_guide/experiments/config.rb +82 -13
- data/lib/trail_guide/experiments/participant.rb +21 -1
- data/lib/trail_guide/helper.rb +59 -32
- data/lib/trail_guide/metrics/checkpoint.rb +24 -0
- data/lib/trail_guide/metrics/config.rb +45 -0
- data/lib/trail_guide/metrics/funnel.rb +24 -0
- data/lib/trail_guide/metrics/goal.rb +89 -0
- data/lib/trail_guide/metrics.rb +9 -0
- data/lib/trail_guide/participant.rb +54 -21
- data/lib/trail_guide/variant.rb +34 -12
- data/lib/trail_guide/version.rb +2 -2
- data/lib/trailguide.rb +4 -0
- metadata +112 -7
- data/app/views/layouts/trail_guide/admin/_footer.erb +0 -10
- data/app/views/layouts/trail_guide/admin/_header.erb +0 -42
- data/app/views/trail_guide/admin/experiments/_combined_experiment.html.erb +0 -189
- data/config/routes.rb +0 -5
data/lib/trail_guide/catalog.rb
CHANGED
@@ -35,15 +35,22 @@ module TrailGuide
|
|
35
35
|
end
|
36
36
|
end
|
37
37
|
|
38
|
+
expgoals = options[:goals]
|
39
|
+
# TODO is it worth parsing these out for complex funnels, etc.? or is
|
40
|
+
# it better to just force use of the DSL?
|
41
|
+
|
38
42
|
DSL.experiment(name) do |config|
|
39
43
|
expvars.each do |expvar|
|
40
44
|
variant *expvar
|
41
45
|
end
|
42
|
-
|
46
|
+
|
47
|
+
expgoals.each do |expgoal|
|
48
|
+
goal expgoal
|
49
|
+
end
|
50
|
+
|
43
51
|
config.control = options[:control] if options[:control]
|
44
|
-
config.
|
52
|
+
config.groups = options[:groups] if options[:groups]
|
45
53
|
config.algorithm = options[:algorithm] if options[:algorithm]
|
46
|
-
config.goals = options[:goals] if options[:goals]
|
47
54
|
config.combined = options[:combined] if options[:combined]
|
48
55
|
config.reset_manually = options[:reset_manually] if options.key?(:reset_manually)
|
49
56
|
config.start_manually = options[:start_manually] if options.key?(:start_manually)
|
@@ -51,6 +58,7 @@ module TrailGuide
|
|
51
58
|
config.track_override = options[:track_override] if options.key?(:track_override)
|
52
59
|
config.allow_multiple_conversions = options[:allow_multiple_conversions] if options.key?(:allow_multiple_conversions)
|
53
60
|
config.allow_multiple_goals = options[:allow_multiple_goals] if options.key?(:allow_multiple_goals)
|
61
|
+
# TODO need to remember to update this with all the new config vars
|
54
62
|
end
|
55
63
|
end
|
56
64
|
end
|
@@ -61,8 +69,8 @@ module TrailGuide
|
|
61
69
|
name: name.to_s.underscore.to_sym,
|
62
70
|
parent: combined,
|
63
71
|
combined: [],
|
64
|
-
variants: combined.configuration.variants.map { |var| var.dup(experiment) }
|
65
|
-
|
72
|
+
variants: combined.configuration.variants.map { |var| var.dup(experiment) },
|
73
|
+
goals: combined.configuration.goals.map { |goal| goal.dup(experiment) }
|
66
74
|
})
|
67
75
|
experiment
|
68
76
|
end
|
@@ -79,6 +87,10 @@ module TrailGuide
|
|
79
87
|
experiments.each(&block)
|
80
88
|
end
|
81
89
|
|
90
|
+
def groups
|
91
|
+
experiments.map(&:groups).flatten.uniq
|
92
|
+
end
|
93
|
+
|
82
94
|
def all
|
83
95
|
exploded = experiments.map do |exp|
|
84
96
|
if exp.combined?
|
@@ -91,48 +103,86 @@ module TrailGuide
|
|
91
103
|
self.class.new(exploded)
|
92
104
|
end
|
93
105
|
|
106
|
+
def calibrating
|
107
|
+
self.class.new(to_a.select(&:calibrating?))
|
108
|
+
end
|
109
|
+
|
94
110
|
def started
|
95
|
-
self.class.new(to_a.select
|
111
|
+
self.class.new(to_a.select { |e| e.started? && !e.winner? })
|
112
|
+
end
|
113
|
+
|
114
|
+
def scheduled
|
115
|
+
self.class.new(to_a.select { |e| e.scheduled? && !e.winner? })
|
96
116
|
end
|
97
117
|
|
98
118
|
def running
|
99
|
-
self.class.new(to_a.select
|
119
|
+
self.class.new(to_a.select { |e| e.running? && !e.winner? })
|
100
120
|
end
|
101
121
|
|
102
122
|
def paused
|
103
|
-
self.class.new(to_a.select
|
123
|
+
self.class.new(to_a.select { |e| e.paused? && !e.winner? })
|
104
124
|
end
|
105
125
|
|
106
126
|
def stopped
|
107
|
-
self.class.new(to_a.select
|
127
|
+
self.class.new(to_a.select { |e| e.stopped? && !e.winner? })
|
108
128
|
end
|
109
129
|
|
110
130
|
def ended
|
111
131
|
self.class.new(to_a.select(&:winner?))
|
112
132
|
end
|
113
133
|
|
134
|
+
def unstarted
|
135
|
+
self.class.new(to_a.select { |e| !e.started? && !e.calibrating? && !e.scheduled? && !e.winner? })
|
136
|
+
end
|
137
|
+
|
114
138
|
def not_running
|
115
139
|
self.class.new(to_a.select { |e| !e.running? })
|
116
140
|
end
|
117
141
|
|
118
142
|
def by_started
|
119
143
|
scoped = to_a.sort do |a,b|
|
120
|
-
if a.
|
121
|
-
|
122
|
-
elsif !a.
|
144
|
+
if !(a.started? || a.scheduled? || a.winner?) && !(b.started? || b.scheduled? || b.winner?)
|
145
|
+
a.experiment_name.to_s <=> b.experiment_name.to_s
|
146
|
+
elsif !(a.started? || a.scheduled? || a.winner?)
|
123
147
|
-1
|
148
|
+
elsif !(b.started? || b.scheduled? || b.winner?)
|
149
|
+
1
|
124
150
|
else
|
125
|
-
if a.
|
151
|
+
if a.winner? && !b.winner?
|
126
152
|
1
|
127
|
-
elsif !a.
|
153
|
+
elsif !a.winner? && b.winner?
|
154
|
+
-1
|
155
|
+
elsif a.winner? && b.winner?
|
156
|
+
a.experiment_name.to_s <=> b.experiment_name.to_s
|
157
|
+
elsif a.running? && !b.running?
|
128
158
|
-1
|
129
|
-
elsif a.
|
159
|
+
elsif !a.running? && b.running?
|
160
|
+
1
|
161
|
+
elsif a.running? && b.running?
|
130
162
|
a.started_at <=> b.started_at
|
163
|
+
elsif a.paused? && !b.paused?
|
164
|
+
-1
|
165
|
+
elsif !a.paused? && b.paused?
|
166
|
+
1
|
167
|
+
elsif a.paused? && b.paused?
|
168
|
+
a.paused_at <=> b.paused_at
|
169
|
+
elsif a.scheduled? && !b.scheduled?
|
170
|
+
-1
|
171
|
+
elsif !a.scheduled? && b.scheduled?
|
172
|
+
1
|
173
|
+
elsif a.scheduled? && b.scheduled?
|
174
|
+
a.started_at <=> b.started_at
|
175
|
+
elsif a.stopped? && !b.stopped?
|
176
|
+
-1
|
177
|
+
elsif !a.stopped? && b.stopped?
|
178
|
+
1
|
179
|
+
elsif a.stopped? && b.stopped?
|
180
|
+
a.stopped_at <=> b.stopped_at
|
131
181
|
else
|
132
|
-
|
182
|
+
a.experiment_name.to_s <=> b.experiment_name.to_s
|
133
183
|
end
|
134
184
|
end
|
135
|
-
end
|
185
|
+
end
|
136
186
|
|
137
187
|
self.class.new(scoped)
|
138
188
|
end
|
@@ -143,7 +193,7 @@ module TrailGuide
|
|
143
193
|
else
|
144
194
|
experiment = experiments.find do |exp|
|
145
195
|
exp.experiment_name == name.to_s.underscore.to_sym ||
|
146
|
-
exp.
|
196
|
+
exp.groups.include?(name.to_s.underscore.to_sym) ||
|
147
197
|
exp.name == name.to_s.classify
|
148
198
|
end
|
149
199
|
return experiment if experiment.present?
|
@@ -165,7 +215,7 @@ module TrailGuide
|
|
165
215
|
# TODO we can be more efficient than mapping twice here
|
166
216
|
selected = experiments.select do |exp|
|
167
217
|
exp.experiment_name == name.to_s.underscore.to_sym ||
|
168
|
-
exp.
|
218
|
+
exp.groups.include?(name.to_s.underscore.to_sym) ||
|
169
219
|
exp.name == name.to_s.classify ||
|
170
220
|
(exp.combined? && exp.combined.any? { |combo| combo.to_s.underscore.to_sym == name.to_s.underscore.to_sym })
|
171
221
|
end.map do |exp|
|
@@ -185,6 +235,70 @@ module TrailGuide
|
|
185
235
|
klass
|
186
236
|
end
|
187
237
|
|
238
|
+
def deregister(key)
|
239
|
+
# TODO (mostly only useful for engine specs)
|
240
|
+
end
|
241
|
+
|
242
|
+
def export
|
243
|
+
map do |exp|
|
244
|
+
if exp.combined?
|
245
|
+
[exp.as_json].concat(exp.combined_experiments.map(&:as_json))
|
246
|
+
else
|
247
|
+
exp.as_json
|
248
|
+
end
|
249
|
+
end.flatten.reduce({}) { |red,exp| red.merge!(exp) }
|
250
|
+
end
|
251
|
+
|
252
|
+
def import(state)
|
253
|
+
state.each do |exp,est|
|
254
|
+
experiment = find(exp)
|
255
|
+
next unless experiment.present?
|
256
|
+
|
257
|
+
experiment.reset!
|
258
|
+
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?
|
262
|
+
TrailGuide.redis.hset(experiment.storage_key, 'winner', est['winner']) if est['winner'].present?
|
263
|
+
|
264
|
+
est['variants'].each do |var,vst|
|
265
|
+
variant = experiment.variants.find { |v| v == var }
|
266
|
+
next unless variant.present?
|
267
|
+
|
268
|
+
TrailGuide.redis.hincrby(variant.storage_key, 'participants', vst['participants'].to_i) if vst['participants'].to_i > 0
|
269
|
+
if vst['converted'].is_a?(Hash)
|
270
|
+
vst['converted'].each do |goal,gct|
|
271
|
+
TrailGuide.redis.hincrby(variant.storage_key, goal, gct.to_i) if gct.to_i > 0
|
272
|
+
end
|
273
|
+
else
|
274
|
+
TrailGuide.redis.hincrby(variant.storage_key, 'converted', vst['converted'].to_i) if vst['converted'].to_i > 0
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
def orphaned(key, trace)
|
281
|
+
added = TrailGuide.redis.sadd("orphans:#{key}", trace)
|
282
|
+
TrailGuide.redis.expire("orphans:#{key}", 15.minutes.seconds)
|
283
|
+
added
|
284
|
+
rescue Errno::ECONNREFUSED, Redis::BaseError, SocketError => e
|
285
|
+
false
|
286
|
+
end
|
287
|
+
|
288
|
+
def orphans
|
289
|
+
TrailGuide.redis.keys("orphans:*").reduce({}) do |h,key|
|
290
|
+
h.merge({ key.split(':').last => TrailGuide.redis.smembers(key) })
|
291
|
+
end
|
292
|
+
rescue Errno::ECONNREFUSED, Redis::BaseError, SocketError => e
|
293
|
+
{}
|
294
|
+
end
|
295
|
+
|
296
|
+
def adopted(key)
|
297
|
+
TrailGuide.redis.del("orphans:#{key}")
|
298
|
+
rescue Errno::ECONNREFUSED, Redis::BaseError, SocketError => e
|
299
|
+
false
|
300
|
+
end
|
301
|
+
|
188
302
|
def method_missing(meth, *args, &block)
|
189
303
|
return experiments.send(meth, *args, &block) if experiments.respond_to?(meth, true)
|
190
304
|
super
|
@@ -199,6 +313,7 @@ module TrailGuide
|
|
199
313
|
klass = opts.delete(:class) || TrailGuide::Experiment
|
200
314
|
Class.new(klass) do
|
201
315
|
configure opts.merge({name: name}), &block
|
316
|
+
register!
|
202
317
|
end
|
203
318
|
end
|
204
319
|
end
|
@@ -10,6 +10,10 @@ module TrailGuide
|
|
10
10
|
@configuration ||= Experiments::CombinedConfig.new(self)
|
11
11
|
end
|
12
12
|
|
13
|
+
def is_combined?
|
14
|
+
true
|
15
|
+
end
|
16
|
+
|
13
17
|
# TODO if just I delegate on this inheriting class, will that override the
|
14
18
|
# defined methods on the base class? and will they interplay nicely? like
|
15
19
|
# with `started?` calling `started_at`, etc.?
|
@@ -44,12 +48,13 @@ module TrailGuide
|
|
44
48
|
end
|
45
49
|
end
|
46
50
|
|
47
|
-
|
48
|
-
|
51
|
+
def parent
|
52
|
+
@parent ||= self.class.parent.new(participant.participant)
|
53
|
+
end
|
49
54
|
|
50
55
|
# use the parent experiment as the algorithm and map to the matching variant
|
51
56
|
def algorithm_choose!(metadata: nil)
|
52
|
-
variant = parent.
|
57
|
+
variant = parent.choose!(metadata: metadata)
|
53
58
|
variants.find { |var| var == variant.name }
|
54
59
|
end
|
55
60
|
end
|
data/lib/trail_guide/config.rb
CHANGED
@@ -4,7 +4,8 @@ module TrailGuide
|
|
4
4
|
:logger, :redis, :disabled, :override_parameter,
|
5
5
|
:allow_multiple_experiments, :adapter, :on_adapter_failover,
|
6
6
|
:filtered_ip_addresses, :filtered_user_agents, :request_filter,
|
7
|
-
:include_helpers, :cleanup_participant_experiments, :unity_ttl
|
7
|
+
:include_helpers, :cleanup_participant_experiments, :unity_ttl,
|
8
|
+
:ignore_orphaned_groups
|
8
9
|
].freeze
|
9
10
|
|
10
11
|
def initialize(*args, **opts, &block)
|
@@ -28,6 +29,10 @@ module TrailGuide
|
|
28
29
|
end
|
29
30
|
end
|
30
31
|
|
32
|
+
def ignore_orphaned_groups?
|
33
|
+
!!self[:ignore_orphaned_groups]
|
34
|
+
end
|
35
|
+
|
31
36
|
def filtered_user_agents
|
32
37
|
@filtered_user_agents ||= begin
|
33
38
|
uas = self[:filtered_user_agents]
|
data/lib/trail_guide/engine.rb
CHANGED
data/lib/trail_guide/errors.rb
CHANGED
@@ -1,7 +1,36 @@
|
|
1
1
|
module TrailGuide
|
2
2
|
class InvalidGoalError < ArgumentError; end
|
3
3
|
class UnsupportedContextError < NoMethodError; end
|
4
|
-
class NoExperimentsError < ArgumentError; end
|
5
4
|
class TooManyExperimentsError < ArgumentError; end
|
6
5
|
class NoVariantMethodError < NoMethodError; end
|
6
|
+
|
7
|
+
class NoExperimentsError < ArgumentError
|
8
|
+
def initialize(key)
|
9
|
+
super("Could not find any experiments matching '#{key}'")
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module Calculators
|
14
|
+
class NoBetaDistributionLibrary < LoadError
|
15
|
+
def initialize(type=nil)
|
16
|
+
if type
|
17
|
+
super("Beta distribution library not found: #{type.to_s}! You must add the '#{type.to_s}' gem to your Gemfile in order to run this analysis with it.")
|
18
|
+
else
|
19
|
+
super("No beta distribution library found! You must add either the 'distribution' or 'rubystats' gems to your Gemfile in order to run this analysis.")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class UnknownBetaDistributionLibrary < ArgumentError
|
25
|
+
def initialize(type)
|
26
|
+
super("Unknown beta distribution library: #{type.to_s}! The libraries available for calculating beta distribution are 'rubystats' (default) or 'distribution'.")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class NoIntegrationLibrary < LoadError
|
31
|
+
def initialize(msg=nil)
|
32
|
+
super(msg || "No integration library found! You must add the 'integration' gem to your gemfile in order to run this analysis.")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
7
36
|
end
|