moses-vanity 1.7.1

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 (98) hide show
  1. data/.autotest +22 -0
  2. data/.gitignore +7 -0
  3. data/.rvmrc +3 -0
  4. data/.travis.yml +13 -0
  5. data/CHANGELOG +374 -0
  6. data/Gemfile +28 -0
  7. data/MIT-LICENSE +21 -0
  8. data/README.rdoc +108 -0
  9. data/Rakefile +189 -0
  10. data/bin/vanity +16 -0
  11. data/doc/_config.yml +2 -0
  12. data/doc/_layouts/_header.html +34 -0
  13. data/doc/_layouts/page.html +47 -0
  14. data/doc/_metrics.textile +12 -0
  15. data/doc/ab_testing.textile +210 -0
  16. data/doc/configuring.textile +45 -0
  17. data/doc/contributing.textile +93 -0
  18. data/doc/credits.textile +23 -0
  19. data/doc/css/page.css +83 -0
  20. data/doc/css/print.css +43 -0
  21. data/doc/css/syntax.css +7 -0
  22. data/doc/email.textile +129 -0
  23. data/doc/experimental.textile +31 -0
  24. data/doc/faq.textile +8 -0
  25. data/doc/identity.textile +43 -0
  26. data/doc/images/ab_in_dashboard.png +0 -0
  27. data/doc/images/clear_winner.png +0 -0
  28. data/doc/images/price_options.png +0 -0
  29. data/doc/images/sidebar_test.png +0 -0
  30. data/doc/images/signup_metric.png +0 -0
  31. data/doc/images/vanity.png +0 -0
  32. data/doc/index.textile +91 -0
  33. data/doc/metrics.textile +231 -0
  34. data/doc/rails.textile +89 -0
  35. data/doc/site.js +27 -0
  36. data/generators/templates/vanity_migration.rb +53 -0
  37. data/generators/vanity_generator.rb +8 -0
  38. data/lib/generators/templates/vanity_migration.rb +53 -0
  39. data/lib/generators/vanity_generator.rb +15 -0
  40. data/lib/vanity.rb +36 -0
  41. data/lib/vanity/adapters/abstract_adapter.rb +140 -0
  42. data/lib/vanity/adapters/active_record_adapter.rb +248 -0
  43. data/lib/vanity/adapters/mock_adapter.rb +157 -0
  44. data/lib/vanity/adapters/mongodb_adapter.rb +178 -0
  45. data/lib/vanity/adapters/redis_adapter.rb +160 -0
  46. data/lib/vanity/backport.rb +26 -0
  47. data/lib/vanity/commands/list.rb +21 -0
  48. data/lib/vanity/commands/report.rb +64 -0
  49. data/lib/vanity/commands/upgrade.rb +34 -0
  50. data/lib/vanity/experiment/ab_test.rb +507 -0
  51. data/lib/vanity/experiment/base.rb +214 -0
  52. data/lib/vanity/frameworks.rb +16 -0
  53. data/lib/vanity/frameworks/rails.rb +318 -0
  54. data/lib/vanity/helpers.rb +66 -0
  55. data/lib/vanity/images/x.gif +0 -0
  56. data/lib/vanity/metric/active_record.rb +85 -0
  57. data/lib/vanity/metric/base.rb +244 -0
  58. data/lib/vanity/metric/google_analytics.rb +83 -0
  59. data/lib/vanity/metric/remote.rb +53 -0
  60. data/lib/vanity/playground.rb +396 -0
  61. data/lib/vanity/templates/_ab_test.erb +28 -0
  62. data/lib/vanity/templates/_experiment.erb +5 -0
  63. data/lib/vanity/templates/_experiments.erb +7 -0
  64. data/lib/vanity/templates/_metric.erb +14 -0
  65. data/lib/vanity/templates/_metrics.erb +13 -0
  66. data/lib/vanity/templates/_report.erb +27 -0
  67. data/lib/vanity/templates/_vanity.js.erb +20 -0
  68. data/lib/vanity/templates/flot.min.js +1 -0
  69. data/lib/vanity/templates/jquery.min.js +19 -0
  70. data/lib/vanity/templates/vanity.css +26 -0
  71. data/lib/vanity/templates/vanity.js +82 -0
  72. data/lib/vanity/version.rb +11 -0
  73. data/test/adapters/redis_adapter_test.rb +17 -0
  74. data/test/experiment/ab_test.rb +771 -0
  75. data/test/experiment/base_test.rb +150 -0
  76. data/test/experiments/age_and_zipcode.rb +19 -0
  77. data/test/experiments/metrics/cheers.rb +3 -0
  78. data/test/experiments/metrics/signups.rb +2 -0
  79. data/test/experiments/metrics/yawns.rb +3 -0
  80. data/test/experiments/null_abc.rb +5 -0
  81. data/test/metric/active_record_test.rb +277 -0
  82. data/test/metric/base_test.rb +293 -0
  83. data/test/metric/google_analytics_test.rb +104 -0
  84. data/test/metric/remote_test.rb +109 -0
  85. data/test/myapp/app/controllers/application_controller.rb +2 -0
  86. data/test/myapp/app/controllers/main_controller.rb +7 -0
  87. data/test/myapp/config/boot.rb +110 -0
  88. data/test/myapp/config/environment.rb +10 -0
  89. data/test/myapp/config/environments/production.rb +0 -0
  90. data/test/myapp/config/routes.rb +3 -0
  91. data/test/passenger_test.rb +43 -0
  92. data/test/playground_test.rb +26 -0
  93. data/test/rails_dashboard_test.rb +37 -0
  94. data/test/rails_helper_test.rb +36 -0
  95. data/test/rails_test.rb +389 -0
  96. data/test/test_helper.rb +145 -0
  97. data/vanity.gemspec +26 -0
  98. metadata +202 -0
@@ -0,0 +1,53 @@
1
+ require "net/http"
2
+ require "cgi"
3
+
4
+ module Vanity
5
+ class Metric
6
+
7
+ # Specifies the base URL to use for a remote metric. For example:
8
+ # metric :sandbox do
9
+ # remote "http://api.vanitydash.com/metrics/sandbox"
10
+ # end
11
+ def remote(url = nil)
12
+ @remote_url = URI.parse(url) if url
13
+ @mutex ||= Mutex.new
14
+ extend Remote
15
+ @remote_url
16
+ end
17
+
18
+ # To update a remote metric, make a POST request to the metric URL with the
19
+ # content type "application/x-www-form-urlencoded" and the following
20
+ # fields:
21
+ # - The +metric+ identifier,
22
+ # - The +timestamp+ must be RFC 2616 formatted (in Ruby just call +httpdate+
23
+ # on the Time object),
24
+ # - The +identity+ (optional),
25
+ # - Pass consecutive values using the field +values[]+, or
26
+ # - Set values by their index using +values[0]+, +values[1]+, etc or
27
+ # - Set values by series name using +values[foo]+, +values[bar]+, etc.
28
+ module Remote
29
+
30
+ def track!(args = nil)
31
+ return unless @playground.collecting?
32
+ timestamp, identity, values = track_args(args)
33
+ params = ["metric=#{CGI.escape @id.to_s}", "timestamp=#{CGI.escape timestamp.httpdate}"]
34
+ params << "identity=#{CGI.escape identity.to_s}" if identity
35
+ params.concat values.map { |v| "values[]=#{v.to_i}" }
36
+ params << @remote_url.query if @remote_url.query
37
+ @mutex.synchronize do
38
+ @http ||= Net::HTTP.start(@remote_url.host, @remote_url.port)
39
+ @http.request Net::HTTP::Post.new(@remote_url.path, "Content-Type"=>"application/x-www-form-urlencoded"), params.join("&")
40
+ end
41
+ rescue Timeout::Error, StandardError
42
+ @playground.logger.error "Error sending data for metric #{name}: #{$!}"
43
+ @http = nil
44
+ ensure
45
+ call_hooks timestamp, identity, values
46
+ end
47
+
48
+ # "Don't worry, be crappy. Revolutionary means you ship and then test."
49
+ # -- Guy Kawazaki
50
+
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,396 @@
1
+ require "uri"
2
+
3
+ module Vanity
4
+
5
+ # Playground catalogs all your experiments, holds the Vanity configuration.
6
+ #
7
+ # @example
8
+ # Vanity.playground.logger = my_logger
9
+ # puts Vanity.playground.map(&:name)
10
+ class Playground
11
+
12
+ DEFAULTS = { :collecting => true, :load_path=>"experiments" }
13
+ DEFAULT_ADD_PARTICIPANT_PATH = '/vanity/add_participant'
14
+
15
+ # Created new Playground. Unless you need to, use the global
16
+ # Vanity.playground.
17
+ #
18
+ # First argument is connection specification (see #redis=), last argument is
19
+ # a set of options, both are optional. Supported options are:
20
+ # - connection -- Connection specification
21
+ # - namespace -- Namespace to use
22
+ # - load_path -- Path to load experiments/metrics from
23
+ # - logger -- Logger to use
24
+ def initialize(*args)
25
+ options = Hash === args.last ? args.pop : {}
26
+ # In the case of Rails, use the Rails logger and collect only for
27
+ # production environment by default.
28
+ defaults = options[:rails] ? DEFAULTS.merge(:collecting => ::Rails.env.production?, :logger => ::Rails.logger) : DEFAULTS
29
+ if config_file_exists?
30
+ env = ENV["RACK_ENV"] || ENV["RAILS_ENV"] || "development"
31
+ config = load_config_file[env]
32
+ if Hash === config
33
+ config = config.inject({}) { |h,kv| h[kv.first.to_sym] = kv.last ; h }
34
+ else
35
+ config = { :connection=>config }
36
+ end
37
+ else
38
+ config = {}
39
+ end
40
+
41
+ @options = defaults.merge(config).merge(options)
42
+ if @options[:host] == 'redis' && @options.values_at(:host, :port, :db).any?
43
+ warn "Deprecated: please specify Redis connection as URL (\"redis://host:port/db\")"
44
+ establish_connection :adapter=>"redis", :host=>@options[:host], :port=>@options[:port], :database=>@options[:db] || @options[:database]
45
+ elsif @options[:redis]
46
+ @adapter = RedisAdapter.new(:redis=>@options[:redis])
47
+ else
48
+ connection_spec = args.shift || @options[:connection]
49
+ if connection_spec
50
+ connection_spec = "redis://" + connection_spec unless connection_spec[/^\w+:/]
51
+ establish_connection connection_spec
52
+ end
53
+ end
54
+
55
+ warn "Deprecated: namespace option no longer supported directly" if @options[:namespace]
56
+ @load_path = @options[:load_path] || DEFAULTS[:load_path]
57
+ unless @logger = @options[:logger]
58
+ @logger = Logger.new(STDOUT)
59
+ @logger.level = Logger::ERROR
60
+ end
61
+ @loading = []
62
+ @use_js = false
63
+ self.add_participant_path = DEFAULT_ADD_PARTICIPANT_PATH
64
+ @collecting = !!@options[:collecting]
65
+ end
66
+
67
+ # Deprecated. Use redis.server instead.
68
+ attr_accessor :host, :port, :db, :password, :namespace
69
+
70
+ # Path to load experiment files from.
71
+ attr_accessor :load_path
72
+
73
+ # Logger.
74
+ attr_accessor :logger
75
+
76
+ # Path to the add_participant action, necessary if you have called use_js!
77
+ attr_accessor :add_participant_path
78
+
79
+ # Defines a new experiment. Generally, do not call this directly,
80
+ # use one of the definition methods (ab_test, measure, etc).
81
+ #
82
+ # @see Vanity::Experiment
83
+ def define(name, type, options = {}, &block)
84
+ warn "Deprecated: if you need this functionality let's make a better API"
85
+ id = name.to_s.downcase.gsub(/\W/, "_").to_sym
86
+ raise "Experiment #{id} already defined once" if experiments[id]
87
+ klass = Experiment.const_get(type.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase })
88
+ experiment = klass.new(self, id, name, options)
89
+ experiment.instance_eval &block
90
+ experiment.save
91
+ experiments[id] = experiment
92
+ end
93
+
94
+ # Returns the experiment. You may not have guessed, but this method raises
95
+ # an exception if it cannot load the experiment's definition.
96
+ #
97
+ # @see Vanity::Experiment
98
+ def experiment(name)
99
+ id = name.to_s.downcase.gsub(/\W/, "_").to_sym
100
+ warn "Deprecated: pleae call experiment method with experiment identifier (a Ruby symbol)" unless id == name
101
+ experiments[id.to_sym] or raise NameError, "No experiment #{id}"
102
+ end
103
+
104
+
105
+ # -- Robot Detection --
106
+
107
+ # Call to indicate that participants should be added via js
108
+ # This helps keep robots from participating in the ab test
109
+ # and skewing results.
110
+ #
111
+ # If you use this, there are two more steps:
112
+ # - Set Vanity.playground.add_participant_path = '/path/to/vanity/action',
113
+ # this should point to the add_participant path that is added with
114
+ # Vanity::Rails::Dashboard, make sure that this action is available
115
+ # to all users
116
+ # - Add <%= vanity_js %> to any page that needs uses an ab_test. vanity_js
117
+ # needs to be included after your call to ab_test so that it knows which
118
+ # version of the experiment the participant is a member of. The helper
119
+ # will render nothing if the there are no ab_tests running on the current
120
+ # page, so adding vanity_js to the bottom of your layouts is a good
121
+ # option. Keep in mind that if you call use_js! and don't include
122
+ # vanity_js in your view no participants will be recorded.
123
+ def use_js!
124
+ @use_js = true
125
+ end
126
+
127
+ def using_js?
128
+ @use_js
129
+ end
130
+
131
+
132
+ # Returns hash of experiments (key is experiment id).
133
+ #
134
+ # @see Vanity::Experiment
135
+ def experiments
136
+ unless @experiments
137
+ @experiments = {}
138
+ @logger.info "Vanity: loading experiments from #{load_path}"
139
+ Dir[File.join(load_path, "*.rb")].each do |file|
140
+ experiment = Experiment::Base.load(self, @loading, file)
141
+ experiment.save
142
+ end
143
+ end
144
+ @experiments
145
+ end
146
+
147
+ # Reloads all metrics and experiments. Rails calls this for each request in
148
+ # development mode.
149
+ def reload!
150
+ @experiments = nil
151
+ @metrics = nil
152
+ load!
153
+ end
154
+
155
+ # Loads all metrics and experiments. Rails calls this during
156
+ # initialization.
157
+ def load!
158
+ experiments
159
+ metrics
160
+ end
161
+
162
+ # Returns a metric (raises NameError if no metric with that identifier).
163
+ #
164
+ # @see Vanity::Metric
165
+ # @since 1.1.0
166
+ def metric(id)
167
+ metrics[id.to_sym] or raise NameError, "No metric #{id}"
168
+ end
169
+
170
+ # True if collection data (metrics and experiments). You only want to
171
+ # collect data in production environment, everywhere else run with
172
+ # collection off.
173
+ #
174
+ # @since 1.4.0
175
+ def collecting?
176
+ @collecting
177
+ end
178
+
179
+ # Turns data collection on and off.
180
+ #
181
+ # @since 1.4.0
182
+ def collecting=(enabled)
183
+ @collecting = !!enabled
184
+ end
185
+
186
+ # Returns hash of metrics (key is metric id).
187
+ #
188
+ # @see Vanity::Metric
189
+ # @since 1.1.0
190
+ def metrics
191
+ unless @metrics
192
+ @metrics = {}
193
+ @logger.info "Vanity: loading metrics from #{load_path}/metrics"
194
+ Dir[File.join(load_path, "metrics/*.rb")].each do |file|
195
+ Metric.load self, @loading, file
196
+ end
197
+ if config_file_exists? && remote = load_config_file["metrics"]
198
+ remote.each do |id, url|
199
+ fail "Metric #{id} already defined in playground" if metrics[id.to_sym]
200
+ metric = Metric.new(self, id)
201
+ metric.remote url
202
+ metrics[id.to_sym] = metric
203
+ end
204
+ end
205
+ end
206
+ @metrics
207
+ end
208
+
209
+ # Tracks an action associated with a metric.
210
+ #
211
+ # @example
212
+ # Vanity.playground.track! :uploaded_video
213
+ #
214
+ # @since 1.1.0
215
+ def track!(id, count = 1)
216
+ metric(id).track! count
217
+ end
218
+
219
+
220
+ # -- Connection management --
221
+
222
+ # This is the preferred way to programmatically create a new connection (or
223
+ # switch to a new connection). If no connection was established, the
224
+ # playground will create a new one by calling this method with no arguments.
225
+ #
226
+ # With no argument, uses the connection specified in config/vanity.yml file
227
+ # for the current environment (RACK_ENV, RAILS_ENV or development). If there
228
+ # is no config/vanity.yml file, picks the configuration from
229
+ # config/redis.yml, or defaults to Redis on localhost, port 6379.
230
+ #
231
+ # If the argument is a symbol, uses the connection specified in
232
+ # config/vanity.yml for that environment. For example:
233
+ # Vanity.playground.establish_connection :production
234
+ #
235
+ # If the argument is a string, it is processed as a URL. For example:
236
+ # Vanity.playground.establish_connection "redis://redis.local/5"
237
+ #
238
+ # Otherwise, the argument is a hash and specifies the adapter name and any
239
+ # additional options understood by that adapter (as with config/vanity.yml).
240
+ # For example:
241
+ # Vanity.playground.establish_connection :adapter=>:redis,
242
+ # :host=>"redis.local"
243
+ #
244
+ # @since 1.4.0
245
+ def establish_connection(spec = nil)
246
+ @spec = spec
247
+ disconnect! if @adapter
248
+ case spec
249
+ when nil
250
+ if config_file_exists?
251
+ env = ENV["RACK_ENV"] || ENV["RAILS_ENV"] || "development"
252
+ spec = load_config_file[env]
253
+ fail "No configuration for #{env}" unless spec
254
+ establish_connection spec
255
+ elsif config_file_exists?("redis.yml")
256
+ env = ENV["RACK_ENV"] || ENV["RAILS_ENV"] || "development"
257
+ redis = load_config_file("redis.yml")[env]
258
+ fail "No configuration for #{env}" unless redis
259
+ establish_connection "redis://" + redis
260
+ else
261
+ establish_connection :adapter=>"redis"
262
+ end
263
+ when Symbol
264
+ spec = load_config_file[spec.to_s]
265
+ establish_connection spec
266
+ when String
267
+ uri = URI.parse(spec)
268
+ params = CGI.parse(uri.query) if uri.query
269
+ establish_connection :adapter=>uri.scheme, :username=>uri.user, :password=>uri.password,
270
+ :host=>uri.host, :port=>uri.port, :path=>uri.path, :params=>params
271
+ else
272
+ spec = spec.inject({}) { |hash,(k,v)| hash[k.to_sym] = v ; hash }
273
+ @adapter = Adapters.establish_connection(spec)
274
+ end
275
+ end
276
+
277
+ def config_file_root
278
+ (defined?(::Rails) ? ::Rails.root : Pathname.new(".")) + "config"
279
+ end
280
+
281
+ def config_file_exists?(basename = "vanity.yml")
282
+ File.exists?(config_file_root + basename)
283
+ end
284
+
285
+ def load_config_file(basename = "vanity.yml")
286
+ YAML.load(ERB.new(File.read(config_file_root + basename)).result)
287
+ end
288
+
289
+ # Returns the current connection. Establishes new connection is necessary.
290
+ #
291
+ # @since 1.4.0
292
+ def connection
293
+ @adapter || establish_connection
294
+ end
295
+
296
+ # Returns true if connection is open.
297
+ #
298
+ # @since 1.4.0
299
+ def connected?
300
+ @adapter && @adapter.active?
301
+ end
302
+
303
+ # Closes the current connection.
304
+ #
305
+ # @since 1.4.0
306
+ def disconnect!
307
+ @adapter.disconnect! if @adapter
308
+ end
309
+
310
+ # Closes the current connection and establishes a new one.
311
+ #
312
+ # @since 1.3.0
313
+ def reconnect!
314
+ establish_connection(@spec)
315
+ end
316
+
317
+ # Deprecated. Use Vanity.playground.collecting = true/false instead. Under
318
+ # Rails, collecting is true in production environment, false in all other
319
+ # environments, which is exactly what you want.
320
+ def test!
321
+ warn "Deprecated: use collecting = false instead"
322
+ self.collecting = false
323
+ end
324
+
325
+ # Deprecated. Use establish_connection or configuration file instead.
326
+ def redis=(spec_or_connection)
327
+ warn "Deprecated: use establish_connection method instead"
328
+ case spec_or_connection
329
+ when String
330
+ establish_connection "redis://" + spec_or_connection
331
+ when ::Redis
332
+ @connection = Adapters::RedisAdapter.new(spec_or_connection)
333
+ when :mock
334
+ establish_connection :adapter=>:mock
335
+ else
336
+ raise "I don't know what to do with #{spec_or_connection.inspect}"
337
+ end
338
+ end
339
+
340
+ def redis
341
+ warn "Deprecated: use connection method instead"
342
+ connection
343
+ end
344
+
345
+ end
346
+
347
+ # In the case of Rails, use the Rails logger and collect only for
348
+ # production environment by default.
349
+ class << self
350
+
351
+ # The playground instance.
352
+ #
353
+ # @see Vanity::Playground
354
+ attr_accessor :playground
355
+ def playground
356
+ # In the case of Rails, use the Rails logger and collect only for
357
+ # production environment by default.
358
+ @playground ||= Playground.new(:rails=>defined?(::Rails))
359
+ end
360
+
361
+ # Returns the Vanity context. For example, when using Rails this would be
362
+ # the current controller, which can be used to get/set the vanity identity.
363
+ def context
364
+ Thread.current[:vanity_context]
365
+ end
366
+
367
+ # Sets the Vanity context. For example, when using Rails this would be
368
+ # set by the set_vanity_context before filter (via Vanity::Rails#use_vanity).
369
+ def context=(context)
370
+ Thread.current[:vanity_context] = context
371
+ end
372
+
373
+ # Path to template.
374
+ def template(name)
375
+ path = File.join(File.dirname(__FILE__), "templates/#{name}")
376
+ path << ".erb" unless name["."]
377
+ path
378
+ end
379
+ end
380
+ end
381
+
382
+
383
+ class Object
384
+
385
+ # Use this method to access an experiment by name.
386
+ #
387
+ # @example
388
+ # puts experiment(:text_size).alternatives
389
+ #
390
+ # @see Vanity::Playground#experiment
391
+ # @deprecated
392
+ def experiment(name)
393
+ warn "Deprecated. Please call Vanity.playground.experiment directly."
394
+ Vanity.playground.experiment(name)
395
+ end
396
+ end
@@ -0,0 +1,28 @@
1
+ <% score = experiment.score %>
2
+ <table>
3
+ <caption>
4
+ <%= experiment.conclusion(score).join(" ") %></caption>
5
+ <% score.alts.each do |alt| %>
6
+ <tr class="<%= "choice" if score.choice == alt %>">
7
+ <td class="option"><%= alt.name.gsub(/^o/, "O") %>:</td>
8
+ <td class="value"><code><%=vanity_h alt.value.to_s %></code></td>
9
+ <td class="value"><%= alt.participants %> participants</td>
10
+ <td class="value"><%= alt.converted %> converted</td>
11
+ <td>
12
+ <%= "%.1f%%" % [alt.conversion_rate * 100] %>
13
+ <%= "(%d%% better than %s)" % [alt.difference, score.least.name] if alt.difference && alt.difference >= 1 %>
14
+ </td>
15
+ <td class="action">
16
+ <% if experiment.active? && respond_to?(:url_for) %>
17
+ <% if experiment.showing?(alt) %>
18
+ showing
19
+ <% else %>
20
+ <a class="button chooses" title="Show me this alternative from now on" href="#"
21
+ data-id="<%= experiment.id %>" data-url="<%= url_for(:action=>:chooses, :e=>experiment.id, :a=>alt.id) %>">show</a>
22
+ <% end %>
23
+ <% end %>
24
+ </td>
25
+ </tr>
26
+ <% end %>
27
+ </table>
28
+ <%= %>