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.
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