trailguide 0.1.31 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +73 -8
  3. data/app/assets/javascripts/trail_guide/admin/application.js +20 -1
  4. data/app/assets/stylesheets/trail_guide/admin/application.css +22 -0
  5. data/app/assets/stylesheets/trail_guide/admin/experiments.css +36 -3
  6. data/app/controllers/trail_guide/admin/application_controller.rb +59 -8
  7. data/app/controllers/trail_guide/admin/experiments_controller.rb +209 -16
  8. data/app/controllers/trail_guide/admin/groups_controller.rb +34 -0
  9. data/app/controllers/trail_guide/experiments_controller.rb +1 -1
  10. data/app/views/layouts/trail_guide/admin/_calculator.erb +24 -0
  11. data/app/views/layouts/trail_guide/admin/_footer.html.erb +27 -0
  12. data/app/views/layouts/trail_guide/admin/_header.html.erb +147 -0
  13. data/app/views/layouts/trail_guide/admin/_import_modal.html.erb +45 -0
  14. data/app/views/layouts/trail_guide/admin/application.html.erb +17 -3
  15. data/app/views/trail_guide/admin/experiments/_alert_peek.html.erb +19 -0
  16. data/app/views/trail_guide/admin/experiments/_alert_state.html.erb +49 -0
  17. data/app/views/trail_guide/admin/experiments/_btn_analyze.html.erb +11 -0
  18. data/app/views/trail_guide/admin/experiments/_btn_analyze_goal.html.erb +5 -0
  19. data/app/views/trail_guide/admin/experiments/_btn_convert.html.erb +33 -0
  20. data/app/views/trail_guide/admin/experiments/_btn_enroll.html.erb +3 -0
  21. data/app/views/trail_guide/admin/experiments/_btn_join.html.erb +11 -0
  22. data/app/views/trail_guide/admin/experiments/_btn_leave.html.erb +7 -0
  23. data/app/views/trail_guide/admin/experiments/_btn_pause.html.erb +5 -0
  24. data/app/views/trail_guide/admin/experiments/_btn_peek.html.erb +13 -0
  25. data/app/views/trail_guide/admin/experiments/_btn_reset.html.erb +5 -0
  26. data/app/views/trail_guide/admin/experiments/_btn_restart.html.erb +5 -0
  27. data/app/views/trail_guide/admin/experiments/_btn_resume.html.erb +5 -0
  28. data/app/views/trail_guide/admin/experiments/_btn_schedule.html.erb +6 -0
  29. data/app/views/trail_guide/admin/experiments/_btn_start.html.erb +5 -0
  30. data/app/views/trail_guide/admin/experiments/_btn_stop.html.erb +5 -0
  31. data/app/views/trail_guide/admin/experiments/_experiment.html.erb +68 -172
  32. data/app/views/trail_guide/admin/experiments/_header.html.erb +87 -0
  33. data/app/views/trail_guide/admin/experiments/_start_modal.html.erb +57 -0
  34. data/app/views/trail_guide/admin/experiments/_tbody.html.erb +112 -0
  35. data/app/views/trail_guide/admin/experiments/_thead.html.erb +38 -0
  36. data/app/views/trail_guide/admin/experiments/index.html.erb +8 -16
  37. data/app/views/trail_guide/admin/experiments/show.html.erb +78 -0
  38. data/app/views/trail_guide/admin/groups/index.html.erb +40 -0
  39. data/app/views/trail_guide/admin/groups/show.html.erb +9 -0
  40. data/app/views/trail_guide/admin/orphans/_alert.html.erb +44 -0
  41. data/config/initializers/trailguide.rb +72 -9
  42. data/config/routes/admin.rb +19 -12
  43. data/lib/trail_guide/admin/engine.rb +2 -0
  44. data/lib/trail_guide/admin.rb +2 -0
  45. data/lib/trail_guide/calculators/bayesian.rb +98 -0
  46. data/lib/trail_guide/calculators/calculator.rb +58 -0
  47. data/lib/trail_guide/calculators/score.rb +68 -0
  48. data/lib/trail_guide/calculators.rb +8 -0
  49. data/lib/trail_guide/catalog.rb +134 -19
  50. data/lib/trail_guide/combined_experiment.rb +8 -3
  51. data/lib/trail_guide/config.rb +6 -1
  52. data/lib/trail_guide/engine.rb +2 -0
  53. data/lib/trail_guide/errors.rb +30 -1
  54. data/lib/trail_guide/experiment.rb +0 -1
  55. data/lib/trail_guide/experiments/base.rb +189 -53
  56. data/lib/trail_guide/experiments/config.rb +82 -13
  57. data/lib/trail_guide/experiments/participant.rb +21 -1
  58. data/lib/trail_guide/helper.rb +59 -32
  59. data/lib/trail_guide/metrics/checkpoint.rb +24 -0
  60. data/lib/trail_guide/metrics/config.rb +45 -0
  61. data/lib/trail_guide/metrics/funnel.rb +24 -0
  62. data/lib/trail_guide/metrics/goal.rb +89 -0
  63. data/lib/trail_guide/metrics.rb +9 -0
  64. data/lib/trail_guide/participant.rb +54 -21
  65. data/lib/trail_guide/variant.rb +34 -12
  66. data/lib/trail_guide/version.rb +2 -2
  67. data/lib/trailguide.rb +4 -0
  68. metadata +112 -7
  69. data/app/views/layouts/trail_guide/admin/_footer.erb +0 -10
  70. data/app/views/layouts/trail_guide/admin/_header.erb +0 -42
  71. data/app/views/trail_guide/admin/experiments/_combined_experiment.html.erb +0 -189
  72. data/config/routes.rb +0 -5
@@ -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
- # TODO also map goals once they're real classes
46
+
47
+ expgoals.each do |expgoal|
48
+ goal expgoal
49
+ end
50
+
43
51
  config.control = options[:control] if options[:control]
44
- config.metric = options[:metric] if options[:metric]
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
- # TODO also map goals once they're separate classes
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(&:started?))
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(&:running?))
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(&:paused?))
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(&:stopped?))
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.running? && !b.running?
121
- 1
122
- elsif !a.running? && b.running?
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.started? && !b.started?
151
+ if a.winner? && !b.winner?
126
152
  1
127
- elsif !a.started? && b.started?
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.started? && b.started?
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
- b.experiment_name.to_s <=> a.experiment_name.to_s
182
+ a.experiment_name.to_s <=> b.experiment_name.to_s
133
183
  end
134
184
  end
135
- end.reverse
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.metric == name.to_s.underscore.to_sym ||
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.metric == name.to_s.underscore.to_sym ||
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
- delegate :parent, to: :class
48
- delegate :running?, :started?, :started_at, :start!, to: :parent
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.new(participant.participant).choose!(metadata: metadata)
57
+ variant = parent.choose!(metadata: metadata)
53
58
  variants.find { |var| var == variant.name }
54
59
  end
55
60
  end
@@ -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]
@@ -6,6 +6,8 @@ module TrailGuide
6
6
  g.test_framework = :rspec
7
7
  end
8
8
 
9
+ paths["config/routes.rb"] = "config/routes/engine.rb"
10
+
9
11
  initializer "trailguide" do |app|
10
12
  TrailGuide::Catalog.load_experiments!
11
13
  if TrailGuide.configuration.include_helpers
@@ -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
@@ -5,7 +5,6 @@ module TrailGuide
5
5
 
6
6
  def self.inherited(child)
7
7
  child.instance_variable_set :@configuration, Experiments::Config.new(child, inherit: self.configuration)
8
- TrailGuide.catalog.register(child)
9
8
  end
10
9
  end
11
10
  end