moses-vanity 1.7.1

Sign up to get free protection for your applications and to get access to all the features.
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
+ <%= %>