trailguide 0.1.31 → 0.2.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 +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
|