vanity 1.8.4 → 1.9.0.beta

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