lookout-vanity 1.8.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (128) hide show
  1. data/.autotest +22 -0
  2. data/.gitignore +10 -0
  3. data/.rvmrc +3 -0
  4. data/.travis.yml +15 -0
  5. data/Appraisals +15 -0
  6. data/CHANGELOG +381 -0
  7. data/Gemfile +25 -0
  8. data/Gemfile.lock +110 -0
  9. data/MIT-LICENSE +21 -0
  10. data/README.rdoc +109 -0
  11. data/Rakefile +169 -0
  12. data/bin/vanity +16 -0
  13. data/doc/_config.yml +2 -0
  14. data/doc/_layouts/_header.html +34 -0
  15. data/doc/_layouts/page.html +47 -0
  16. data/doc/_metrics.textile +12 -0
  17. data/doc/ab_testing.textile +210 -0
  18. data/doc/configuring.textile +45 -0
  19. data/doc/contributing.textile +93 -0
  20. data/doc/credits.textile +23 -0
  21. data/doc/css/page.css +83 -0
  22. data/doc/css/print.css +43 -0
  23. data/doc/css/syntax.css +7 -0
  24. data/doc/email.textile +129 -0
  25. data/doc/experimental.textile +31 -0
  26. data/doc/faq.textile +8 -0
  27. data/doc/identity.textile +43 -0
  28. data/doc/images/ab_in_dashboard.png +0 -0
  29. data/doc/images/clear_winner.png +0 -0
  30. data/doc/images/price_options.png +0 -0
  31. data/doc/images/sidebar_test.png +0 -0
  32. data/doc/images/signup_metric.png +0 -0
  33. data/doc/images/vanity.png +0 -0
  34. data/doc/index.textile +91 -0
  35. data/doc/metrics.textile +231 -0
  36. data/doc/rails.textile +89 -0
  37. data/doc/site.js +27 -0
  38. data/gemfiles/rails3.gemfile +20 -0
  39. data/gemfiles/rails3.gemfile.lock +135 -0
  40. data/gemfiles/rails31.gemfile +20 -0
  41. data/gemfiles/rails31.gemfile.lock +146 -0
  42. data/gemfiles/rails32.gemfile +20 -0
  43. data/gemfiles/rails32.gemfile.lock +144 -0
  44. data/generators/templates/vanity_migration.rb +53 -0
  45. data/generators/vanity_generator.rb +8 -0
  46. data/lib/generators/templates/vanity_migration.rb +53 -0
  47. data/lib/generators/vanity_generator.rb +15 -0
  48. data/lib/vanity.rb +36 -0
  49. data/lib/vanity/adapters/abstract_adapter.rb +145 -0
  50. data/lib/vanity/adapters/active_record_adapter.rb +263 -0
  51. data/lib/vanity/adapters/mock_adapter.rb +157 -0
  52. data/lib/vanity/adapters/mongodb_adapter.rb +178 -0
  53. data/lib/vanity/adapters/redis_adapter.rb +160 -0
  54. data/lib/vanity/backport.rb +26 -0
  55. data/lib/vanity/commands/list.rb +21 -0
  56. data/lib/vanity/commands/report.rb +64 -0
  57. data/lib/vanity/commands/upgrade.rb +34 -0
  58. data/lib/vanity/experiment/ab_test.rb +582 -0
  59. data/lib/vanity/experiment/base.rb +218 -0
  60. data/lib/vanity/frameworks.rb +16 -0
  61. data/lib/vanity/frameworks/rails.rb +325 -0
  62. data/lib/vanity/helpers.rb +71 -0
  63. data/lib/vanity/images/x.gif +0 -0
  64. data/lib/vanity/metric/active_record.rb +93 -0
  65. data/lib/vanity/metric/base.rb +244 -0
  66. data/lib/vanity/metric/google_analytics.rb +83 -0
  67. data/lib/vanity/metric/remote.rb +53 -0
  68. data/lib/vanity/playground.rb +408 -0
  69. data/lib/vanity/templates/_ab_test.erb +28 -0
  70. data/lib/vanity/templates/_experiment.erb +5 -0
  71. data/lib/vanity/templates/_experiments.erb +7 -0
  72. data/lib/vanity/templates/_metric.erb +14 -0
  73. data/lib/vanity/templates/_metrics.erb +13 -0
  74. data/lib/vanity/templates/_report.erb +27 -0
  75. data/lib/vanity/templates/_vanity.js.erb +20 -0
  76. data/lib/vanity/templates/flot.min.js +1 -0
  77. data/lib/vanity/templates/jquery.min.js +19 -0
  78. data/lib/vanity/templates/vanity.css +26 -0
  79. data/lib/vanity/templates/vanity.js +82 -0
  80. data/lib/vanity/version.rb +11 -0
  81. data/lookout-vanity.gemspec +26 -0
  82. data/test/adapters/redis_adapter_test.rb +17 -0
  83. data/test/dummy/Rakefile +7 -0
  84. data/test/dummy/app/controllers/application_controller.rb +3 -0
  85. data/test/dummy/app/helpers/application_helper.rb +2 -0
  86. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  87. data/test/dummy/config.ru +4 -0
  88. data/test/dummy/config/application.rb +44 -0
  89. data/test/dummy/config/boot.rb +10 -0
  90. data/test/dummy/config/database.yml +5 -0
  91. data/test/dummy/config/environment.rb +5 -0
  92. data/test/dummy/config/environments/development.rb +26 -0
  93. data/test/dummy/config/environments/production.rb +49 -0
  94. data/test/dummy/config/environments/test.rb +35 -0
  95. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  96. data/test/dummy/config/initializers/inflections.rb +10 -0
  97. data/test/dummy/config/initializers/mime_types.rb +5 -0
  98. data/test/dummy/config/initializers/secret_token.rb +7 -0
  99. data/test/dummy/config/initializers/session_store.rb +8 -0
  100. data/test/dummy/config/locales/en.yml +5 -0
  101. data/test/dummy/config/routes.rb +58 -0
  102. data/test/dummy/public/favicon.ico +0 -0
  103. data/test/dummy/public/stylesheets/.gitkeep +0 -0
  104. data/test/dummy/script/rails +6 -0
  105. data/test/experiment/ab_test.rb +859 -0
  106. data/test/experiment/base_test.rb +150 -0
  107. data/test/experiments/age_and_zipcode.rb +19 -0
  108. data/test/experiments/metrics/cheers.rb +3 -0
  109. data/test/experiments/metrics/signups.rb +2 -0
  110. data/test/experiments/metrics/yawns.rb +3 -0
  111. data/test/experiments/null_abc.rb +5 -0
  112. data/test/metric/active_record_test.rb +307 -0
  113. data/test/metric/base_test.rb +293 -0
  114. data/test/metric/google_analytics_test.rb +104 -0
  115. data/test/metric/remote_test.rb +109 -0
  116. data/test/myapp/app/controllers/application_controller.rb +2 -0
  117. data/test/myapp/app/controllers/main_controller.rb +7 -0
  118. data/test/myapp/config/boot.rb +110 -0
  119. data/test/myapp/config/environment.rb +10 -0
  120. data/test/myapp/config/environments/production.rb +0 -0
  121. data/test/myapp/config/routes.rb +3 -0
  122. data/test/passenger_test.rb +45 -0
  123. data/test/playground_test.rb +26 -0
  124. data/test/rails_dashboard_test.rb +37 -0
  125. data/test/rails_helper_test.rb +38 -0
  126. data/test/rails_test.rb +412 -0
  127. data/test/test_helper.rb +168 -0
  128. metadata +268 -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,408 @@
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
+
99
+ def experiment(name)
100
+ id = name.to_s.downcase.gsub(/\W/, "_").to_sym
101
+ warn "Deprecated: pleae call experiment method with experiment identifier (a Ruby symbol)" unless id == name
102
+ experiments[id.to_sym] or raise NoExperimentError, "No experiment #{id}"
103
+ end
104
+
105
+
106
+ # -- Robot Detection --
107
+
108
+ # Call to indicate that participants should be added via js
109
+ # This helps keep robots from participating in the ab test
110
+ # and skewing results.
111
+ #
112
+ # If you use this, there are two more steps:
113
+ # - Set Vanity.playground.add_participant_path = '/path/to/vanity/action',
114
+ # this should point to the add_participant path that is added with
115
+ # Vanity::Rails::Dashboard, make sure that this action is available
116
+ # to all users
117
+ # - Add <%= vanity_js %> to any page that needs uses an ab_test. vanity_js
118
+ # needs to be included after your call to ab_test so that it knows which
119
+ # version of the experiment the participant is a member of. The helper
120
+ # will render nothing if the there are no ab_tests running on the current
121
+ # page, so adding vanity_js to the bottom of your layouts is a good
122
+ # option. Keep in mind that if you call use_js! and don't include
123
+ # vanity_js in your view no participants will be recorded.
124
+ def use_js!
125
+ @use_js = true
126
+ end
127
+
128
+ def using_js?
129
+ @use_js
130
+ end
131
+
132
+
133
+ # Returns hash of experiments (key is experiment id).
134
+ #
135
+ # @see Vanity::Experiment
136
+ def experiments
137
+ unless @experiments
138
+ @experiments = {}
139
+ @logger.info "Vanity: loading experiments from #{load_path}"
140
+ Dir[File.join(load_path, "*.rb")].each do |file|
141
+ experiment = Experiment::Base.load(self, @loading, file)
142
+ experiment.save
143
+ end
144
+ end
145
+ @experiments
146
+ end
147
+
148
+ # Reloads all metrics and experiments. Rails calls this for each request in
149
+ # development mode.
150
+ def reload!
151
+ @experiments = nil
152
+ @metrics = nil
153
+ load!
154
+ end
155
+
156
+ # Loads all metrics and experiments. Rails calls this during
157
+ # initialization.
158
+ def load!
159
+ experiments
160
+ metrics
161
+ end
162
+
163
+ # Returns a metric (raises NameError if no metric with that identifier).
164
+ #
165
+ # @see Vanity::Metric
166
+ # @since 1.1.0
167
+ def metric(id)
168
+ metrics[id.to_sym] or raise NameError, "No metric #{id}"
169
+ end
170
+
171
+ # True if collection data (metrics and experiments). You only want to
172
+ # collect data in production environment, everywhere else run with
173
+ # collection off.
174
+ #
175
+ # @since 1.4.0
176
+ def collecting?
177
+ @collecting
178
+ end
179
+
180
+ # Turns data collection on and off.
181
+ #
182
+ # @since 1.4.0
183
+ def collecting=(enabled)
184
+ @collecting = !!enabled
185
+ end
186
+
187
+ # Returns hash of metrics (key is metric id).
188
+ #
189
+ # @see Vanity::Metric
190
+ # @since 1.1.0
191
+ def metrics
192
+ unless @metrics
193
+ @metrics = {}
194
+ @logger.info "Vanity: loading metrics from #{load_path}/metrics"
195
+ Dir[File.join(load_path, "metrics/*.rb")].each do |file|
196
+ Metric.load self, @loading, file
197
+ end
198
+ if config_file_exists? && remote = load_config_file["metrics"]
199
+ remote.each do |id, url|
200
+ fail "Metric #{id} already defined in playground" if metrics[id.to_sym]
201
+ metric = Metric.new(self, id)
202
+ metric.remote url
203
+ metrics[id.to_sym] = metric
204
+ end
205
+ end
206
+ end
207
+ @metrics
208
+ end
209
+
210
+ # Tracks an action associated with a metric.
211
+ #
212
+ # @example
213
+ # Vanity.playground.track! :uploaded_video
214
+ #
215
+ # @since 1.1.0
216
+ def track!(id, count = 1)
217
+ metric(id).track! count
218
+ end
219
+
220
+ # Choose a value for any experiment tagged with the specified event;
221
+ # filter can be used to specify a white-list for experiments.
222
+ def assign_on(event, filter=nil)
223
+ experiments.each do |id, obj|
224
+ if !filter || filter.include?(id)
225
+ if obj.assign_on?(event)
226
+ obj.choose
227
+ end
228
+ end
229
+ end
230
+ end
231
+
232
+ # -- Connection management --
233
+
234
+ # This is the preferred way to programmatically create a new connection (or
235
+ # switch to a new connection). If no connection was established, the
236
+ # playground will create a new one by calling this method with no arguments.
237
+ #
238
+ # With no argument, uses the connection specified in config/vanity.yml file
239
+ # for the current environment (RACK_ENV, RAILS_ENV or development). If there
240
+ # is no config/vanity.yml file, picks the configuration from
241
+ # config/redis.yml, or defaults to Redis on localhost, port 6379.
242
+ #
243
+ # If the argument is a symbol, uses the connection specified in
244
+ # config/vanity.yml for that environment. For example:
245
+ # Vanity.playground.establish_connection :production
246
+ #
247
+ # If the argument is a string, it is processed as a URL. For example:
248
+ # Vanity.playground.establish_connection "redis://redis.local/5"
249
+ #
250
+ # Otherwise, the argument is a hash and specifies the adapter name and any
251
+ # additional options understood by that adapter (as with config/vanity.yml).
252
+ # For example:
253
+ # Vanity.playground.establish_connection :adapter=>:redis,
254
+ # :host=>"redis.local"
255
+ #
256
+ # @since 1.4.0
257
+ def establish_connection(spec = nil)
258
+ @spec = spec
259
+ disconnect! if @adapter
260
+ case spec
261
+ when nil
262
+ if config_file_exists?
263
+ env = ENV["RACK_ENV"] || ENV["RAILS_ENV"] || "development"
264
+ spec = load_config_file[env]
265
+ fail "No configuration for #{env}" unless spec
266
+ establish_connection spec
267
+ elsif config_file_exists?("redis.yml")
268
+ env = ENV["RACK_ENV"] || ENV["RAILS_ENV"] || "development"
269
+ redis = load_config_file("redis.yml")[env]
270
+ fail "No configuration for #{env}" unless redis
271
+ establish_connection "redis://" + redis
272
+ else
273
+ establish_connection :adapter=>"redis"
274
+ end
275
+ when Symbol
276
+ spec = load_config_file[spec.to_s]
277
+ establish_connection spec
278
+ when String
279
+ uri = URI.parse(spec)
280
+ params = CGI.parse(uri.query) if uri.query
281
+ establish_connection :adapter=>uri.scheme, :username=>uri.user, :password=>uri.password,
282
+ :host=>uri.host, :port=>uri.port, :path=>uri.path, :params=>params
283
+ else
284
+ spec = spec.inject({}) { |hash,(k,v)| hash[k.to_sym] = v ; hash }
285
+ @adapter = Adapters.establish_connection(spec)
286
+ end
287
+ end
288
+
289
+ def config_file_root
290
+ (defined?(::Rails) ? ::Rails.root : Pathname.new(".")) + "config"
291
+ end
292
+
293
+ def config_file_exists?(basename = "vanity.yml")
294
+ File.exists?(config_file_root + basename)
295
+ end
296
+
297
+ def load_config_file(basename = "vanity.yml")
298
+ YAML.load(ERB.new(File.read(config_file_root + basename)).result)
299
+ end
300
+
301
+ # Returns the current connection. Establishes new connection is necessary.
302
+ #
303
+ # @since 1.4.0
304
+ def connection
305
+ @adapter || establish_connection
306
+ end
307
+
308
+ # Returns true if connection is open.
309
+ #
310
+ # @since 1.4.0
311
+ def connected?
312
+ @adapter && @adapter.active?
313
+ end
314
+
315
+ # Closes the current connection.
316
+ #
317
+ # @since 1.4.0
318
+ def disconnect!
319
+ @adapter.disconnect! if @adapter
320
+ end
321
+
322
+ # Closes the current connection and establishes a new one.
323
+ #
324
+ # @since 1.3.0
325
+ def reconnect!
326
+ establish_connection(@spec)
327
+ end
328
+
329
+ # Deprecated. Use Vanity.playground.collecting = true/false instead. Under
330
+ # Rails, collecting is true in production environment, false in all other
331
+ # environments, which is exactly what you want.
332
+ def test!
333
+ warn "Deprecated: use collecting = false instead"
334
+ self.collecting = false
335
+ end
336
+
337
+ # Deprecated. Use establish_connection or configuration file instead.
338
+ def redis=(spec_or_connection)
339
+ warn "Deprecated: use establish_connection method instead"
340
+ case spec_or_connection
341
+ when String
342
+ establish_connection "redis://" + spec_or_connection
343
+ when ::Redis
344
+ @connection = Adapters::RedisAdapter.new(spec_or_connection)
345
+ when :mock
346
+ establish_connection :adapter=>:mock
347
+ else
348
+ raise "I don't know what to do with #{spec_or_connection.inspect}"
349
+ end
350
+ end
351
+
352
+ def redis
353
+ warn "Deprecated: use connection method instead"
354
+ connection
355
+ end
356
+
357
+ end
358
+
359
+ # In the case of Rails, use the Rails logger and collect only for
360
+ # production environment by default.
361
+ class << self
362
+
363
+ # The playground instance.
364
+ #
365
+ # @see Vanity::Playground
366
+ attr_accessor :playground
367
+ def playground
368
+ # In the case of Rails, use the Rails logger and collect only for
369
+ # production environment by default.
370
+ @playground ||= Playground.new(:rails=>defined?(::Rails))
371
+ end
372
+
373
+ # Returns the Vanity context. For example, when using Rails this would be
374
+ # the current controller, which can be used to get/set the vanity identity.
375
+ def context
376
+ Thread.current[:vanity_context]
377
+ end
378
+
379
+ # Sets the Vanity context. For example, when using Rails this would be
380
+ # set by the set_vanity_context before filter (via Vanity::Rails#use_vanity).
381
+ def context=(context)
382
+ Thread.current[:vanity_context] = context
383
+ end
384
+
385
+ # Path to template.
386
+ def template(name)
387
+ path = File.join(File.dirname(__FILE__), "templates/#{name}")
388
+ path << ".erb" unless name["."]
389
+ path
390
+ end
391
+ end
392
+ end
393
+
394
+
395
+ class Object
396
+
397
+ # Use this method to access an experiment by name.
398
+ #
399
+ # @example
400
+ # puts experiment(:text_size).alternatives
401
+ #
402
+ # @see Vanity::Playground#experiment
403
+ # @deprecated
404
+ def experiment(name)
405
+ warn "Deprecated. Please call Vanity.playground.experiment directly."
406
+ Vanity.playground.experiment(name)
407
+ end
408
+ end