vanity 1.8.4 → 1.9.0.beta

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. data/.travis.yml +3 -2
  2. data/CHANGELOG +12 -0
  3. data/Gemfile +6 -3
  4. data/Gemfile.lock +12 -10
  5. data/README.rdoc +45 -16
  6. data/Rakefile +14 -9
  7. data/doc/_layouts/page.html +4 -6
  8. data/doc/ab_testing.textile +1 -1
  9. data/doc/configuring.textile +2 -4
  10. data/doc/email.textile +1 -3
  11. data/doc/index.textile +3 -63
  12. data/doc/rails.textile +34 -8
  13. data/gemfiles/rails3.gemfile +12 -3
  14. data/gemfiles/rails3.gemfile.lock +37 -11
  15. data/gemfiles/rails31.gemfile +12 -3
  16. data/gemfiles/rails31.gemfile.lock +37 -11
  17. data/gemfiles/rails32.gemfile +12 -3
  18. data/gemfiles/rails32.gemfile.lock +37 -11
  19. data/gemfiles/rails4.gemfile +12 -3
  20. data/gemfiles/rails4.gemfile.lock +37 -11
  21. data/lib/vanity/adapters/abstract_adapter.rb +4 -0
  22. data/lib/vanity/adapters/active_record_adapter.rb +18 -10
  23. data/lib/vanity/adapters/mock_adapter.rb +8 -4
  24. data/lib/vanity/adapters/mongodb_adapter.rb +11 -7
  25. data/lib/vanity/adapters/redis_adapter.rb +88 -37
  26. data/lib/vanity/commands/report.rb +9 -9
  27. data/lib/vanity/experiment/ab_test.rb +120 -101
  28. data/lib/vanity/experiment/alternative.rb +21 -21
  29. data/lib/vanity/experiment/base.rb +5 -5
  30. data/lib/vanity/experiment/bayesian_bandit_score.rb +51 -51
  31. data/lib/vanity/experiment/definition.rb +10 -10
  32. data/lib/vanity/frameworks/rails.rb +39 -36
  33. data/lib/vanity/helpers.rb +6 -4
  34. data/lib/vanity/metric/active_record.rb +1 -1
  35. data/lib/vanity/metric/base.rb +23 -24
  36. data/lib/vanity/metric/google_analytics.rb +5 -5
  37. data/lib/vanity/playground.rb +118 -24
  38. data/lib/vanity/templates/_report.erb +20 -6
  39. data/lib/vanity/templates/vanity.css +2 -0
  40. data/lib/vanity/version.rb +1 -1
  41. data/test/adapters/redis_adapter_test.rb +106 -1
  42. data/test/dummy/config/database.yml +21 -4
  43. data/test/dummy/config/routes.rb +1 -1
  44. data/test/experiment/ab_test.rb +93 -13
  45. data/test/metric/active_record_test.rb +9 -4
  46. data/test/passenger_test.rb +43 -42
  47. data/test/playground_test.rb +50 -1
  48. data/test/rails_dashboard_test.rb +38 -1
  49. data/test/rails_helper_test.rb +5 -0
  50. data/test/rails_test.rb +66 -15
  51. data/test/test_helper.rb +24 -2
  52. data/vanity.gemspec +0 -2
  53. metadata +45 -57
@@ -1,4 +1,4 @@
1
- module Vanity
1
+ module Vanity
2
2
  # Helper methods available on Object.
3
3
  #
4
4
  # @example From ERB template
@@ -17,7 +17,7 @@ module Vanity
17
17
  # end
18
18
  # end
19
19
  module Helpers
20
-
20
+
21
21
  # This method returns one of the alternative values in the named A/B test.
22
22
  #
23
23
  # @example A/B two alternatives for a page
@@ -34,12 +34,14 @@ module Vanity
34
34
  # end
35
35
  # @since 1.2.0
36
36
  def ab_test(name, &block)
37
+ # TODO refactor with Vanity::Rails::Helpers#ab_test
38
+ request = respond_to?(:request) ? self.request : nil
37
39
  if Vanity.playground.using_js?
38
40
  @_vanity_experiments ||= {}
39
- @_vanity_experiments[name] ||= Vanity.playground.experiment(name).choose
41
+ @_vanity_experiments[name] ||= Vanity.playground.experiment(name).choose(request)
40
42
  value = @_vanity_experiments[name].value
41
43
  else
42
- value = Vanity.playground.experiment(name).choose.value
44
+ value = Vanity.playground.experiment(name).choose(request).value
43
45
  end
44
46
 
45
47
  if block
@@ -3,7 +3,7 @@ module Vanity
3
3
 
4
4
  AGGREGATES = [:average, :minimum, :maximum, :sum]
5
5
 
6
- # Use an ActiveRecord model to get metric data from database table. Also
6
+ # Use an ActiveRecord model to get metric data from database table. Also
7
7
  # forwards +after_create+ callbacks to hooks (updating experiments).
8
8
  #
9
9
  # Supported options:
@@ -1,11 +1,11 @@
1
1
  module Vanity
2
2
 
3
- # A metric is an object that implements two methods: +name+ and +values+. It
3
+ # A metric is an object that implements two methods: +name+ and +values+. It
4
4
  # can also respond to addition methods (+track!+, +bounds+, etc), these are
5
5
  # optional.
6
6
  #
7
7
  # This class implements a basic metric that tracks data and stores it in the
8
- # database. You can use this as the basis for your metric, or as reference
8
+ # database. You can use this as the basis for your metric, or as reference
9
9
  # for the methods your metric must and can implement.
10
10
  #
11
11
  # @since 1.1.0
@@ -20,7 +20,7 @@ module Vanity
20
20
  # description "Most boring metric ever"
21
21
  # end
22
22
  module Definition
23
-
23
+
24
24
  attr_reader :playground
25
25
 
26
26
  # Defines a new metric, using the class Vanity::Metric.
@@ -37,7 +37,7 @@ module Vanity
37
37
  end
38
38
 
39
39
  end
40
-
40
+
41
41
  # Startup metrics for pirates. AARRR stands for:
42
42
  # * Acquisition
43
43
  # * Activation
@@ -45,15 +45,14 @@ module Vanity
45
45
  # * Referral
46
46
  # * Revenue
47
47
  # Read more: http://500hats.typepad.com/500blogs/2007/09/startup-metrics.html
48
-
49
48
  class << self
50
49
 
51
50
  # Helper method to return description for a metric.
52
51
  #
53
52
  # A metric object may have a +description+ method that returns a detailed
54
- # description. It may also have no description, or no +description+
53
+ # description. It may also have no description, or no +description+
55
54
  # method, in which case return +nil+.
56
- #
55
+ #
57
56
  # @example
58
57
  # puts Vanity::Metric.description(metric)
59
58
  def description(metric)
@@ -63,25 +62,25 @@ module Vanity
63
62
  # Helper method to return bounds for a metric.
64
63
  #
65
64
  # A metric object may have a +bounds+ method that returns lower and upper
66
- # bounds. It may also have no bounds, or no +bounds+ # method, in which
65
+ # bounds. It may also have no bounds, or no +bounds+ # method, in which
67
66
  # case we return +[nil, nil]+.
68
- #
67
+ #
69
68
  # @example
70
69
  # upper = Vanity::Metric.bounds(metric).last
71
70
  def bounds(metric)
72
71
  metric.respond_to?(:bounds) && metric.bounds || [nil, nil]
73
72
  end
74
73
 
75
- # Returns data set for a given date range. The data set is an array of
74
+ # Returns data set for a given date range. The data set is an array of
76
75
  # date, value pairs.
77
76
  #
78
- # First argument is the metric. Second argument is the start date, or
79
- # number of days to go back in history, defaults to 90 days. Third
77
+ # First argument is the metric. Second argument is the start date, or
78
+ # number of days to go back in history, defaults to 90 days. Third
80
79
  # argument is end date, defaults to today.
81
80
  #
82
81
  # @example These are all equivalent:
83
- # Vanity::Metric.data(my_metric)
84
- # Vanity::Metric.data(my_metric, 90)
82
+ # Vanity::Metric.data(my_metric)
83
+ # Vanity::Metric.data(my_metric, 90)
85
84
  # Vanity::Metric.data(my_metric, Date.today - 89)
86
85
  # Vanity::Metric.data(my_metric, Date.today - 89, Date.today)
87
86
  def data(metric, *args)
@@ -158,7 +157,7 @@ module Vanity
158
157
  end
159
158
  protected :track_args
160
159
 
161
- # Metric definitions use this to introduce tracking hook. The hook is
160
+ # Metric definitions use this to introduce tracking hook. The hook is
162
161
  # called with metric identifier, timestamp, count and possibly additional
163
162
  # arguments.
164
163
  #
@@ -171,27 +170,27 @@ module Vanity
171
170
  end
172
171
 
173
172
  # This method returns the acceptable bounds of a metric as an array with
174
- # two values: low and high. Use nil for unbounded.
173
+ # two values: low and high. Use nil for unbounded.
175
174
  #
176
- # Alerts are created when metric values exceed their bounds. For example,
175
+ # Alerts are created when metric values exceed their bounds. For example,
177
176
  # a metric of user registration can use historical data to calculate
178
- # expected range of new registration for the next day. If actual metric
177
+ # expected range of new registration for the next day. If actual metric
179
178
  # falls below the expected range, it could indicate registration process is
180
- # broken. Going above higher bound could trigger opening a Champagne
179
+ # broken. Going above higher bound could trigger opening a Champagne
181
180
  # bottle.
182
181
  #
183
182
  # The default implementation returns +nil+.
184
183
  def bounds
185
184
  end
186
-
185
+
187
186
 
188
187
  # -- Reporting --
189
-
190
- # Human readable metric name. All metrics must implement this method.
188
+
189
+ # Human readable metric name. All metrics must implement this method.
191
190
  attr_reader :name
192
191
  alias :to_s :name
193
192
 
194
- # Human readable description. Use two newlines to break paragraphs.
193
+ # Human readable description. Use two newlines to break paragraphs.
195
194
  attr_accessor :description
196
195
 
197
196
  # Sets or returns description. For example
@@ -206,7 +205,7 @@ module Vanity
206
205
  end
207
206
 
208
207
  # Given two arguments, a start date and an end date (inclusive), returns an
209
- # array of measurements. All metrics must implement this method.
208
+ # array of measurements. All metrics must implement this method.
210
209
  def values(from, to)
211
210
  values = connection.metric_values(@id, from, to)
212
211
  values.map { |row| row.first.to_i }
@@ -1,9 +1,9 @@
1
1
  module Vanity
2
2
  class Metric
3
-
4
- # Use Google Analytics metric. Note: you must +require "garb"+ before
3
+
4
+ # Use Google Analytics metric. Note: you must +require "garb"+ before
5
5
  # vanity.
6
- #
6
+ #
7
7
  # @example Page views
8
8
  # metric "Page views" do
9
9
  # google_analytics "UA-1828623-6"
@@ -40,7 +40,7 @@ module Vanity
40
40
  end
41
41
  (from..to).map { |day| data[day.strftime('%Y%m%d')] || 0 }
42
42
  end
43
-
43
+
44
44
  # Hooks not supported for GA metrics.
45
45
  def hook
46
46
  fail "Cannot use hooks with Google Analytics methods"
@@ -59,7 +59,7 @@ module Vanity
59
59
  end
60
60
 
61
61
  class Resource
62
- # GA profile used for this report. Populated after calling results.
62
+ # GA profile used for this report. Populated after calling results.
63
63
  attr_reader :profile
64
64
 
65
65
  def initialize(web_property_id, metric)
@@ -16,7 +16,7 @@ module Vanity
16
16
  # Vanity.playground.
17
17
  #
18
18
  # First argument is connection specification (see #redis=), last argument is
19
- # a set of options, both are optional. Supported options are:
19
+ # a set of options, both are optional. Supported options are:
20
20
  # - connection -- Connection specification
21
21
  # - namespace -- Namespace to use
22
22
  # - load_path -- Path to load experiments/metrics from
@@ -26,7 +26,7 @@ module Vanity
26
26
  options = Hash === args.last ? args.pop : {}
27
27
  # In the case of Rails, use the Rails logger and collect only for
28
28
  # production environment by default.
29
- defaults = options[:rails] ? DEFAULTS.merge(:collecting => ::Rails.env.production?, :logger => ::Rails.logger) : DEFAULTS
29
+ defaults = options[:rails] ? DEFAULTS.merge(:collecting => true, :logger => ::Rails.logger) : DEFAULTS
30
30
  if config_file_exists?
31
31
  env = ENV["RACK_ENV"] || ENV["RAILS_ENV"] || "development"
32
32
  config = load_config_file[env]
@@ -52,6 +52,7 @@ module Vanity
52
52
 
53
53
  @loading = []
54
54
  @use_js = false
55
+ @failover_on_datastore_error = false
55
56
  self.add_participant_path = DEFAULT_ADD_PARTICIPANT_PATH
56
57
  @collecting = !!@options[:collecting]
57
58
  end
@@ -65,9 +66,13 @@ module Vanity
65
66
  # Logger.
66
67
  attr_accessor :logger
67
68
 
68
- # Path to the add_participant action, necessary if you have called use_js!
69
+ # Path to the add_participant action.
69
70
  attr_accessor :add_participant_path
70
71
 
72
+ attr_accessor :on_datastore_error
73
+
74
+ attr_accessor :request_filter
75
+
71
76
  # Defines a new experiment. Generally, do not call this directly,
72
77
  # use one of the definition methods (ab_test, measure, etc).
73
78
  #
@@ -87,7 +92,6 @@ module Vanity
87
92
  # an exception if it cannot load the experiment's definition.
88
93
  #
89
94
  # @see Vanity::Experiment
90
-
91
95
  def experiment(name)
92
96
  id = name.to_s.downcase.gsub(/\W/, "_").to_sym
93
97
  warn "Deprecated: pleae call experiment method with experiment identifier (a Ruby symbol)" unless id == name
@@ -102,33 +106,35 @@ module Vanity
102
106
  # when converted to_s (so could be used for caching, for example)
103
107
  def participant_info(participant_id)
104
108
  participant_array = []
105
- experiments.values.sort_by{|e| e.name}.each do |e|
109
+ experiments.values.sort_by(&:name).each do |e|
106
110
  index = connection.ab_assigned(e.id, participant_id)
107
111
  if index
108
112
  participant_array << [e, e.alternatives[index.to_i]]
109
113
  end
110
114
  end
111
- return participant_array
115
+ participant_array
112
116
  end
113
117
 
118
+
114
119
  # -- Robot Detection --
115
120
 
116
- # Call to indicate that participants should be added via js
117
- # This helps keep robots from participating in the ab test
118
- # and skewing results.
121
+ # Call to indicate that participants should be added via js. This helps
122
+ # keep robots from participating in the A/B test and skewing results.
119
123
  #
120
- # If you use this, there are two more steps:
121
- # - Set Vanity.playground.add_participant_path = '/path/to/vanity/action',
122
- # this should point to the add_participant path that is added with
123
- # Vanity::Rails::Dashboard, make sure that this action is available
124
- # to all users
124
+ # If you want to use this:
125
125
  # - Add <%= vanity_js %> to any page that needs uses an ab_test. vanity_js
126
126
  # needs to be included after your call to ab_test so that it knows which
127
- # version of the experiment the participant is a member of. The helper
127
+ # version of the experiment the participant is a member of. The helper
128
128
  # will render nothing if the there are no ab_tests running on the current
129
129
  # page, so adding vanity_js to the bottom of your layouts is a good
130
- # option. Keep in mind that if you call use_js! and don't include
130
+ # option. Keep in mind that if you call use_js! and don't include
131
131
  # vanity_js in your view no participants will be recorded.
132
+ #
133
+ # Note that a custom JS callback path can be set using:
134
+ # - Set Vanity.playground.add_participant_path = '/path/to/vanity/action',
135
+ # this should point to the add_participant path that is added with
136
+ # Vanity::Rails::Dashboard, make sure that this action is available
137
+ # to all users.
132
138
  def use_js!
133
139
  @use_js = true
134
140
  end
@@ -138,7 +144,91 @@ module Vanity
138
144
  end
139
145
 
140
146
 
141
- # Returns hash of experiments (key is experiment id).
147
+ # -- Datastore graceful failover --
148
+
149
+ # Turns on passing of errors to the Proc returned by #on_datastore_error.
150
+ # Call Vanity.playground.failover_on_datastore_error! to turn this on.
151
+ #
152
+ # @since 1.9.0
153
+ def failover_on_datastore_error!
154
+ @failover_on_datastore_error = true
155
+ end
156
+
157
+ # Returns whether to failover on an error raise by the datastore adapter.
158
+ #
159
+ # @since 1.9.0
160
+ def failover_on_datastore_error?
161
+ @failover_on_datastore_error
162
+ end
163
+
164
+ # Must return a Proc that accepts as parameters: the thrown error, the
165
+ # calling Class, the calling method, and an array of arguments passed to
166
+ # the calling method. The return value is ignored.
167
+ #
168
+ # Proc.new do |error, klass, method, arguments|
169
+ # ...
170
+ # end
171
+ #
172
+ # The default implementation logs this information to Playground#logger.
173
+ #
174
+ # Set a custom action by calling Vanity.playground.on_datastore_error =
175
+ # Proc.new { ... }.
176
+ #
177
+ # @since 1.9.0
178
+ def on_datastore_error
179
+ @on_datastore_error || default_on_datastore_error
180
+ end
181
+
182
+ def default_on_datastore_error # :nodoc:
183
+ Proc.new do |error, klass, method, arguments|
184
+ log = "[#{Time.now.iso8601}]"
185
+ log << " [vanity #{klass} #{method}]"
186
+ log << " [#{error.message}]"
187
+ log << " [#{arguments.join(' ')}]"
188
+ @logger.error(log)
189
+ nil
190
+ end
191
+ end
192
+ protected :default_on_datastore_error
193
+
194
+
195
+ # -- Blocking or ignoring visitors --
196
+
197
+ # Must return a Proc that accepts as a parameter the request object, if
198
+ # made available by the implement framework. The return value should be a
199
+ # boolean whether to ignore the request. This is called only for the JS
200
+ # callback action.
201
+ #
202
+ # Proc.new do |request|
203
+ # ...
204
+ # end
205
+ #
206
+ # The default implementation does a simple test of whether the request's
207
+ # HTTP_USER_AGENT header contains a URI, since well behaved bots typically
208
+ # include a reference URI in their user agent strings. (Original idea:
209
+ # http://stackoverflow.com/a/9285889.)
210
+ #
211
+ # Alternatively, one could filter an explicit list of IPs, add additional
212
+ # user agent strings to filter, or any custom test. Set a custom filter
213
+ # by calling Vanity.playground.request_filter = Proc.new { ... }.
214
+ #
215
+ # @since 1.9.0
216
+ def request_filter
217
+ @request_filter || default_request_filter
218
+ end
219
+
220
+ def default_request_filter # :nodoc:
221
+ Proc.new do |request|
222
+ request &&
223
+ request.env &&
224
+ request.env["HTTP_USER_AGENT"] &&
225
+ request.env["HTTP_USER_AGENT"].match(/\(.*https?:\/\/.*\)/)
226
+ end
227
+ end
228
+ protected :default_request_filter
229
+
230
+ # Returns hash of experiments (key is experiment id). This create the
231
+ # Experiment and persists it to the datastore.
142
232
  #
143
233
  # @see Vanity::Experiment
144
234
  def experiments
@@ -153,7 +243,11 @@ module Vanity
153
243
  @experiments
154
244
  end
155
245
 
156
- # Reloads all metrics and experiments. Rails calls this for each request in
246
+ def experiments_persisted?
247
+ experiments.keys.all? { |id| connection.experiment_persisted?(id) }
248
+ end
249
+
250
+ # Reloads all metrics and experiments. Rails calls this for each request in
157
251
  # development mode.
158
252
  def reload!
159
253
  @experiments = nil
@@ -161,7 +255,7 @@ module Vanity
161
255
  load!
162
256
  end
163
257
 
164
- # Loads all metrics and experiments. Rails calls this during
258
+ # Loads all metrics and experiments. Rails calls this during
165
259
  # initialization.
166
260
  def load!
167
261
  experiments
@@ -323,9 +417,7 @@ module Vanity
323
417
  establish_connection(@spec)
324
418
  end
325
419
 
326
- # Deprecated. Use Vanity.playground.collecting = true/false instead. Under
327
- # Rails, collecting is true in production environment, false in all other
328
- # environments, which is exactly what you want.
420
+ # Deprecated. Use Vanity.playground.collecting = true/false instead.
329
421
  def test!
330
422
  warn "Deprecated: use collecting = false instead"
331
423
  self.collecting = false
@@ -364,6 +456,8 @@ module Vanity
364
456
  if connection_spec
365
457
  connection_spec = "redis://" + connection_spec unless connection_spec[/^\w+:/]
366
458
  establish_connection connection_spec
459
+ else
460
+ establish_connection
367
461
  end
368
462
  end
369
463
  end
@@ -384,13 +478,13 @@ module Vanity
384
478
  @playground ||= Playground.new(:rails=>defined?(::Rails))
385
479
  end
386
480
 
387
- # Returns the Vanity context. For example, when using Rails this would be
481
+ # Returns the Vanity context. For example, when using Rails this would be
388
482
  # the current controller, which can be used to get/set the vanity identity.
389
483
  def context
390
484
  Thread.current[:vanity_context]
391
485
  end
392
486
 
393
- # Sets the Vanity context. For example, when using Rails this would be
487
+ # Sets the Vanity context. For example, when using Rails this would be
394
488
  # set by the set_vanity_context before filter (via Vanity::Rails#use_vanity).
395
489
  def context=(context)
396
490
  Thread.current[:vanity_context] = context
@@ -13,14 +13,28 @@
13
13
  </head>
14
14
  <body>
15
15
  <div class="vanity">
16
- <% experiments = Vanity.playground.experiments ; unless experiments.empty? %>
17
- <h2>Experiments</h2>
18
- <%= render :file=>Vanity.template("_experiments"), :locals=>{:experiments=>experiments} %>
16
+ <% unless Vanity.playground.collecting? %>
17
+ <div class="alert">
18
+ Vanity is currently not collecting data or metrics. To turn on data collection, set <span style='font-family: courier'>Vanity.playground.collecting = true;</span> in <span style='font-family: courier'>config/environments/[environment].rb</span>.
19
+ </div>
19
20
  <% end %>
20
- <% metrics = Vanity.playground.metrics ; unless metrics.empty? %>
21
- <h2>Metrics</h2>
22
- <%= render :file=>Vanity.template("_metrics"), :locals=>{:metrics=>metrics, :experiments=>experiments} %>
21
+
22
+ <% if @experiments_persisted %>
23
+ <% if @experiments.present? %>
24
+ <h2>Experiments</h2>
25
+ <%= render :file=>Vanity.template("_experiments"), :locals=>{:experiments=>@experiments} %>
26
+ <% end %>
27
+
28
+ <% unless @metrics.empty? %>
29
+ <h2>Metrics</h2>
30
+ <%= render :file=>Vanity.template("_metrics"), :locals=>{:metrics=>@metrics, :experiments=>@experiments} %>
31
+ <% end %>
32
+ <% else %>
33
+ <div class="alert">
34
+ Vanity's cached experiments are out of sync with those on the filesystem and/or those in the datastore. Please restart your server and/or turn on collecting.
35
+ </div>
23
36
  <% end %>
37
+
24
38
  <p class="footer">Generated by <a href="http://vanity.labnotes.org">Vanity</a></p>
25
39
  </div>
26
40
  </body>