vanity 1.0.0 → 1.1.0

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 (57) hide show
  1. data/CHANGELOG +35 -0
  2. data/README.rdoc +33 -6
  3. data/lib/vanity.rb +13 -7
  4. data/lib/vanity/backport.rb +43 -0
  5. data/lib/vanity/commands/report.rb +13 -3
  6. data/lib/vanity/experiment/ab_test.rb +98 -66
  7. data/lib/vanity/experiment/base.rb +51 -5
  8. data/lib/vanity/metric.rb +213 -0
  9. data/lib/vanity/mock_redis.rb +76 -0
  10. data/lib/vanity/playground.rb +78 -61
  11. data/lib/vanity/rails/dashboard.rb +11 -2
  12. data/lib/vanity/rails/helpers.rb +3 -3
  13. data/lib/vanity/templates/_ab_test.erb +3 -4
  14. data/lib/vanity/templates/_experiment.erb +4 -4
  15. data/lib/vanity/templates/_experiments.erb +2 -2
  16. data/lib/vanity/templates/_metric.erb +9 -0
  17. data/lib/vanity/templates/_metrics.erb +13 -0
  18. data/lib/vanity/templates/_report.erb +14 -3
  19. data/lib/vanity/templates/flot.min.js +1 -0
  20. data/lib/vanity/templates/jquery.min.js +19 -0
  21. data/lib/vanity/templates/vanity.css +16 -4
  22. data/lib/vanity/templates/vanity.js +96 -0
  23. data/test/ab_test_test.rb +159 -96
  24. data/test/experiment_test.rb +99 -18
  25. data/test/experiments/age_and_zipcode.rb +1 -0
  26. data/test/experiments/metrics/cheers.rb +3 -0
  27. data/test/experiments/metrics/signups.rb +2 -0
  28. data/test/experiments/metrics/yawns.rb +3 -0
  29. data/test/experiments/null_abc.rb +1 -0
  30. data/test/metric_test.rb +287 -0
  31. data/test/playground_test.rb +1 -80
  32. data/test/rails_test.rb +9 -6
  33. data/test/test_helper.rb +37 -6
  34. data/vanity.gemspec +1 -1
  35. data/vendor/{redis-0.1 → redis-rb}/LICENSE +0 -0
  36. data/vendor/{redis-0.1 → redis-rb}/README.markdown +0 -0
  37. data/vendor/{redis-0.1 → redis-rb}/Rakefile +0 -0
  38. data/vendor/redis-rb/bench.rb +44 -0
  39. data/vendor/redis-rb/benchmarking/suite.rb +24 -0
  40. data/vendor/redis-rb/benchmarking/worker.rb +71 -0
  41. data/vendor/redis-rb/bin/distredis +33 -0
  42. data/vendor/redis-rb/examples/basic.rb +16 -0
  43. data/vendor/redis-rb/examples/incr-decr.rb +18 -0
  44. data/vendor/redis-rb/examples/list.rb +26 -0
  45. data/vendor/redis-rb/examples/sets.rb +36 -0
  46. data/vendor/{redis-0.1 → redis-rb}/lib/dist_redis.rb +0 -0
  47. data/vendor/{redis-0.1 → redis-rb}/lib/hash_ring.rb +0 -0
  48. data/vendor/{redis-0.1 → redis-rb}/lib/pipeline.rb +0 -2
  49. data/vendor/{redis-0.1 → redis-rb}/lib/redis.rb +25 -7
  50. data/vendor/{redis-0.1 → redis-rb}/lib/redis/raketasks.rb +0 -0
  51. data/vendor/redis-rb/profile.rb +22 -0
  52. data/vendor/redis-rb/redis-rb.gemspec +30 -0
  53. data/vendor/{redis-0.1 → redis-rb}/spec/redis_spec.rb +113 -0
  54. data/vendor/{redis-0.1 → redis-rb}/spec/spec_helper.rb +0 -0
  55. data/vendor/redis-rb/speed.rb +16 -0
  56. data/vendor/{redis-0.1 → redis-rb}/tasks/redis.tasks.rb +5 -1
  57. metadata +37 -14
@@ -1,6 +1,29 @@
1
1
  module Vanity
2
2
  module Experiment
3
3
 
4
+ # These methods are available from experiment definitions (files located in
5
+ # the experiments directory, automatically loaded by Vanity). Use these
6
+ # methods to define you experiments, for example:
7
+ # ab_test "New Banner" do
8
+ # alternatives :red, :green, :blue
9
+ # metrics :signup
10
+ # end
11
+ module Definition
12
+
13
+ # Defines a new experiment, given the experiment's name, type and
14
+ # definition block.
15
+ def define(name, type, options = nil, &block)
16
+ options ||= {}
17
+ @playground.define(name, type, options, &block)
18
+ end
19
+
20
+ def binding(playground)
21
+ @playground = playground
22
+ Kernel.binding
23
+ end
24
+
25
+ end
26
+
4
27
  # Base class that all experiment types are derived from.
5
28
  class Base
6
29
 
@@ -12,6 +35,27 @@ module Vanity
12
35
  name.split("::").last.gsub(/([a-z])([A-Z])/) { "#{$1}_#{$2}" }.gsub(/([A-Z])([A-Z][a-z])/) { "#{$1}_#{$2}" }.downcase
13
36
  end
14
37
 
38
+ # Playground uses this to load experiment definitions.
39
+ def load(playground, stack, path, id)
40
+ fn = File.join(path, "#{id}.rb")
41
+ fail "Circular dependency detected: #{stack.join('=>')}=>#{fn}" if stack.include?(fn)
42
+ source = File.read(fn)
43
+ stack.push fn
44
+ context = Object.new
45
+ context.instance_eval do
46
+ extend Definition
47
+ experiment = eval(source, context.binding(playground), fn)
48
+ fail NameError.new("Expected #{fn} to define experiment #{id}", id) unless experiment.id == id
49
+ experiment
50
+ end
51
+ rescue
52
+ error = NameError.exception($!.message, id)
53
+ error.set_backtrace $!.backtrace
54
+ raise error
55
+ ensure
56
+ stack.pop
57
+ end
58
+
15
59
  end
16
60
 
17
61
  def initialize(playground, id, name, options, &block)
@@ -19,7 +63,7 @@ module Vanity
19
63
  @id, @name = id.to_sym, name
20
64
  @options = options || {}
21
65
  @namespace = "#{@playground.namespace}:#{@id}"
22
- @identify_block = ->(context){ context.vanity_identity }
66
+ @identify_block = lambda { |context| context.vanity_identity }
23
67
  end
24
68
 
25
69
  # Human readable experiment name (first argument you pass when creating a
@@ -110,18 +154,19 @@ module Vanity
110
154
  # Force experiment to complete.
111
155
  def complete!
112
156
  redis.setnx key(:completed_at), Time.now.to_i
113
- # TODO: logging
157
+ @completed_at = redis[key(:completed_at)]
158
+ @playground.logger.info "vanity: completed experiment #{id}"
114
159
  end
115
160
 
116
161
  # Time stamp when experiment was completed.
117
162
  def completed_at
118
- time = redis[key(:completed_at)]
119
- time && Time.at(time.to_i)
163
+ @completed_at ||= redis[key(:completed_at)]
164
+ @completed_at && Time.at(@completed_at.to_i)
120
165
  end
121
166
 
122
167
  # Returns true if experiment active, false if completed.
123
168
  def active?
124
- redis[key(:completed_at)].nil?
169
+ !redis.exists(key(:completed_at))
125
170
  end
126
171
 
127
172
  # -- Store/validate --
@@ -130,6 +175,7 @@ module Vanity
130
175
  def destroy
131
176
  redis.del key(:created_at)
132
177
  redis.del key(:completed_at)
178
+ @created_at = @completed_at = nil
133
179
  end
134
180
 
135
181
  # Called by Playground to save the experiment definition.
@@ -0,0 +1,213 @@
1
+ module Vanity
2
+
3
+ # A metric is an object that implements two methods: +name+ and +values+. It
4
+ # can also respond to addition methods (+track!+, +bounds+, etc), these are
5
+ # optional.
6
+ #
7
+ # This class implements a basic metric that tracks data and stores it in
8
+ # Redis. You can use this as the basis for your metric, or as reference for
9
+ # the methods your metric must and can implement.
10
+ #
11
+ # @since 1.1.0
12
+ class Metric
13
+
14
+ # These methods are available when defining a metric in a file loaded
15
+ # from the +experiments/metrics+ directory.
16
+ #
17
+ # For example:
18
+ # $ cat experiments/metrics/yawn_sec
19
+ # metric "Yawns/sec" do
20
+ # description "Most boring metric ever"
21
+ # end
22
+ module Definition
23
+
24
+ # Defines a new metric, using the class Vanity::Metric.
25
+ def metric(name, &block)
26
+ metric = Metric.new(@playground, name.to_s, name.to_s.downcase.gsub(/\W/, "_"))
27
+ metric.instance_eval &block
28
+ metric
29
+ end
30
+
31
+ def binding(playground)
32
+ @playground = playground
33
+ Kernel.binding
34
+ end
35
+
36
+ end
37
+
38
+ # Startup metrics for pirates. AARRR stands for:
39
+ # * Acquisition
40
+ # * Activation
41
+ # * Retention
42
+ # * Referral
43
+ # * Revenue
44
+ # Read more: http://500hats.typepad.com/500blogs/2007/09/startup-metrics.html
45
+
46
+ class << self
47
+
48
+ # Helper method to return description for a metric.
49
+ #
50
+ # A metric object may have a +description+ method that returns a detailed
51
+ # description. It may also have no description, or no +description+
52
+ # method, in which case return +nil+.
53
+ #
54
+ # @example
55
+ # puts Vanity::Metric.description(metric)
56
+ def description(metric)
57
+ metric.description if metric.respond_to?(:description)
58
+ end
59
+
60
+ # Helper method to return bounds for a metric.
61
+ #
62
+ # A metric object may have a +bounds+ method that returns lower and upper
63
+ # bounds. It may also have no bounds, or no +bounds+ # method, in which
64
+ # case we return +[nil, nil]+.
65
+ #
66
+ # @example
67
+ # upper = Vanity::Metric.bounds(metric).last
68
+ def bounds(metric)
69
+ metric.respond_to?(:bounds) && metric.bounds || [nil, nil]
70
+ end
71
+
72
+ # Returns data set for a given date range. The data set is an array of
73
+ # date, value pairs.
74
+ #
75
+ # First argument is the metric. Second argument is the start date, or
76
+ # number of days to go back in history, defaults to 90 days. Third
77
+ # argument is end date, defaults to today.
78
+ #
79
+ # @example These are all equivalent:
80
+ # Vanity::Metric.data(my_metric)
81
+ # Vanity::Metric.data(my_metric, 90)
82
+ # Vanity::Metric.data(my_metric, Date.today - 90)
83
+ # Vanity::Metric.data(my_metric, Date.today - 90, Date.today)
84
+ def data(metric, *args)
85
+ first = args.shift || 90
86
+ to = args.shift || Date.today
87
+ from = first.respond_to?(:to_date) ? first.to_date : to - first
88
+ (from..to).zip(metric.values(from, to))
89
+ end
90
+
91
+ # Playground uses this to load metric definitions.
92
+ def load(playground, stack, path, id)
93
+ fn = File.join(path, "#{id}.rb")
94
+ fail "Circular dependency detected: #{stack.join('=>')}=>#{fn}" if stack.include?(fn)
95
+ source = File.read(fn)
96
+ stack.push fn
97
+ context = Object.new
98
+ context.instance_eval do
99
+ extend Definition
100
+ metric = eval(source, context.binding(playground), fn)
101
+ fail NameError.new("Expected #{fn} to define metric #{id}", id) unless metric.name.downcase.gsub(/\W+/, '_').to_sym == id
102
+ metric
103
+ end
104
+ rescue
105
+ error = NameError.exception($!.message, id)
106
+ error.set_backtrace $!.backtrace
107
+ raise error
108
+ ensure
109
+ stack.pop
110
+ end
111
+
112
+ end
113
+
114
+
115
+ # Takes playground (need this to access Redis), friendly name and optional
116
+ # id (can infer from name).
117
+ def initialize(playground, name, id = nil)
118
+ id ||= name.to_s.downcase.gsub(/\W+/, '_')
119
+ @playground, @name, @id = playground, name.to_s, id.to_sym
120
+ @hooks = []
121
+ redis.setnx key(:created_at), Time.now.to_i
122
+ @created_at = Time.at(redis[key(:created_at)].to_i)
123
+ end
124
+
125
+
126
+ # -- Tracking --
127
+
128
+ # Called to track an action associated with this metric.
129
+ def track!(count = 1)
130
+ timestamp = Time.now
131
+ if count > 0
132
+ redis.incrby key(timestamp.to_date, "count"), count
133
+ @playground.logger.info "vanity: #{@id} with count #{count}"
134
+ @hooks.each do |hook|
135
+ hook.call @id, timestamp, count
136
+ end
137
+ end
138
+ end
139
+
140
+ # Metric definitions use this to introduce tracking hook. The hook is
141
+ # called with metric identifier, timestamp, count and possibly additional
142
+ # arguments.
143
+ #
144
+ # For example:
145
+ # hook do |metric_id, timestamp, count|
146
+ # syslog.info metric_id
147
+ # end
148
+ def hook(&block)
149
+ @hooks << block
150
+ end
151
+
152
+ # This method returns the acceptable bounds of a metric as an array with
153
+ # two values: low and high. Use nil for unbounded.
154
+ #
155
+ # Alerts are created when metric values exceed their bounds. For example,
156
+ # a metric of user registration can use historical data to calculate
157
+ # expected range of new registration for the next day. If actual metric
158
+ # falls below the expected range, it could indicate registration process is
159
+ # broken. Going above higher bound could trigger opening a Champagne
160
+ # bottle.
161
+ #
162
+ # The default implementation returns +nil+.
163
+ def bounds
164
+ end
165
+
166
+
167
+ # -- Reporting --
168
+
169
+ # Human readable metric name. All metrics must implement this method.
170
+ def name
171
+ @name
172
+ end
173
+
174
+ # Time stamp when metric was created.
175
+ attr_reader :created_at
176
+
177
+ # Human readable description. Use two newlines to break paragraphs.
178
+ attr_accessor :description
179
+
180
+ # Sets or returns description. For example
181
+ # metric "Yawns/sec" do
182
+ # description "Most boring metric ever"
183
+ # end
184
+ #
185
+ # puts "Just defined: " + metric(:boring).description
186
+ def description(text = nil)
187
+ @description = text if text
188
+ @description
189
+ end
190
+
191
+ # Given two arguments, a start date and an end date (inclusive), returns an
192
+ # array of measurements. All metrics must implement this method.
193
+ def values(from, to)
194
+ redis.mget((from.to_date..to.to_date).map { |date| key(date, "count") }).map(&:to_i)
195
+ end
196
+
197
+
198
+ # -- Storage --
199
+
200
+ def destroy!
201
+ redis.del redis.keys(key("*"))
202
+ end
203
+
204
+ def redis
205
+ @playground.redis
206
+ end
207
+
208
+ def key(*args)
209
+ "metrics:#{@id}:#{args.join(':')}"
210
+ end
211
+
212
+ end
213
+ end
@@ -0,0 +1,76 @@
1
+ module Vanity
2
+ # The Redis you should never use in production.
3
+ class MockRedis
4
+ @@hash = {}
5
+
6
+ def initialize(options = {})
7
+ end
8
+
9
+ def [](key)
10
+ @@hash[key]
11
+ end
12
+
13
+ def []=(key, value)
14
+ @@hash[key] = value.to_s
15
+ end
16
+
17
+ def del(*keys)
18
+ keys.flatten.each do |key|
19
+ @@hash.delete key
20
+ end
21
+ end
22
+
23
+ def setnx(key, value)
24
+ @@hash[key] = value.to_s unless @@hash.has_key?(key)
25
+ end
26
+
27
+ def incr(key)
28
+ @@hash[key] = (@@hash[key].to_i + 1).to_s
29
+ end
30
+
31
+ def incrby(key, value)
32
+ @@hash[key] = (@@hash[key].to_i + value).to_s
33
+ end
34
+
35
+ def mget(keys)
36
+ @@hash.values_at(*keys)
37
+ end
38
+
39
+ def exists(key)
40
+ @@hash.has_key?(key)
41
+ end
42
+
43
+ def keys(pattern)
44
+ regexp = Regexp.new(pattern.split("*").map { |r| Regexp.escape(r) }.join(".*"))
45
+ @@hash.keys.select { |key| key =~ regexp }
46
+ end
47
+
48
+ def flushdb
49
+ @@hash.clear
50
+ end
51
+
52
+ def sismember(key, value)
53
+ case set = @@hash[key]
54
+ when nil ; false
55
+ when Set ; set.member?(value)
56
+ else fail "Not a set"
57
+ end
58
+ end
59
+
60
+ def sadd(key, value)
61
+ case set = @@hash[key]
62
+ when nil ; @@hash[key] = Set.new([value])
63
+ when Set ; set.add value
64
+ else fail "Not a set"
65
+ end
66
+ end
67
+
68
+ def scard(key)
69
+ case set = @@hash[key]
70
+ when nil ; 0
71
+ when Set ; set.size
72
+ else fail "Not a set"
73
+ end
74
+ end
75
+ end
76
+ end
@@ -1,35 +1,23 @@
1
1
  module Vanity
2
2
 
3
- # These methods are available from experiment definitions (files located in
4
- # the experiments directory, automatically loaded by Vanity). Use these
5
- # methods to define you experiments, for example:
6
- # ab_test "New Banner" do
7
- # alternatives :red, :green, :blue
8
- # end
9
- module Definition
10
-
11
- protected
12
- # Defines a new experiment, given the experiment's name, type and
13
- # definition block.
14
- def define(name, type, options = nil, &block)
15
- options ||= {}
16
- Vanity.playground.define(name, type, options, &block)
17
- end
18
-
19
- end
20
-
21
3
  # Playground catalogs all your experiments, holds the Vanity configuration.
22
- # For example:
4
+ #
5
+ # @example
23
6
  # Vanity.playground.logger = my_logger
24
7
  # puts Vanity.playground.map(&:name)
25
8
  class Playground
26
9
 
10
+ DEFAULTS = { :host=>"127.0.0.1", :port=>6379, :db=>0, :load_path=>"experiments" }
11
+
27
12
  # Created new Playground. Unless you need to, use the global Vanity.playground.
28
- def initialize
29
- @experiments = {}
30
- @host, @port, @db = "127.0.0.1", 6379, 0
13
+ def initialize(options = {})
14
+ @host, @port, @db, @load_path = DEFAULTS.merge(options).values_at(:host, :port, :db, :load_path)
31
15
  @namespace = "vanity:#{Vanity::Version::MAJOR}"
32
- @load_path = "experiments"
16
+ @logger = options[:logger] || Logger.new(STDOUT)
17
+ @logger.level = Logger::ERROR
18
+ @redis = options[:redis]
19
+ @experiments = {}
20
+ @loading = []
33
21
  end
34
22
 
35
23
  # Redis host name. Default is 127.0.0.1
@@ -56,7 +44,7 @@ module Vanity
56
44
  # Defines a new experiment. Generally, do not call this directly,
57
45
  # use one of the definition methods (ab_test, measure, etc).
58
46
  def define(name, type, options = {}, &block)
59
- id = name.to_s.downcase.gsub(/\W/, "_")
47
+ id = name.to_s.downcase.gsub(/\W/, "_").to_sym
60
48
  raise "Experiment #{id} already defined once" if @experiments[id]
61
49
  klass = Experiment.const_get(type.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase })
62
50
  experiment = klass.new(self, id, name, options)
@@ -65,41 +53,19 @@ module Vanity
65
53
  @experiments[id] = experiment
66
54
  end
67
55
 
68
- # Returns the named experiment. You may not have guessed, but this method
69
- # raises an exception if it cannot load the experiment's definition.
70
- #
71
- # Experiment names are always mapped by downcasing them and replacing
72
- # non-word characters with underscores, so "Green call to action" becomes
73
- # "green_call_to_action". You can also use a symbol if you feel like it.
56
+ # Returns the experiment. You may not have guessed, but this method raises
57
+ # an exception if it cannot load the experiment's definition.
74
58
  def experiment(name)
75
- id = name.to_s.downcase.gsub(/\W/, "_")
76
- unless @experiments.has_key?(id)
77
- @loading ||= []
78
- fail "Circular dependency detected: #{@loading.join('=>')}=>#{id}" if @loading.include?(id)
79
- begin
80
- @loading.push id
81
- source = File.read(File.expand_path("#{id}.rb", load_path))
82
- context = Object.new
83
- context.instance_eval do
84
- extend Definition
85
- eval source
86
- end
87
- rescue
88
- error = LoadError.exception($!.message)
89
- error.set_backtrace $!.backtrace
90
- raise error
91
- ensure
92
- @loading.pop
93
- end
94
- end
95
- @experiments[id] or fail LoadError, "Expected experiments/#{id}.rb to define experiment #{name}"
59
+ id = name.to_s.downcase.gsub(/\W/, "_").to_sym
60
+ warn "Deprecated: pleae call experiment method with experiment identifier (a Ruby symbol)" unless id == name
61
+ @experiments[id] ||= Experiment::Base.load(self, @loading, File.expand_path(load_path), id)
96
62
  end
97
63
 
98
64
  # Returns list of all loaded experiments.
99
65
  def experiments
100
66
  Dir[File.join(load_path, "*.rb")].each do |file|
101
67
  id = File.basename(file).gsub(/.rb$/, "")
102
- experiment id
68
+ experiment id.to_sym
103
69
  end
104
70
  @experiments.values
105
71
  end
@@ -107,25 +73,72 @@ module Vanity
107
73
  # Reloads all experiments.
108
74
  def reload!
109
75
  @experiments.clear
76
+ @metrics = nil
110
77
  end
111
78
 
112
79
  # Use this instance to access the Redis database.
113
80
  def redis
114
- redis = Redis.new(host: self.host, port: self.port, db: self.db,
115
- password: self.password, logger: self.logger)
116
- class << self ; self ; end.send(:define_method, :redis) { redis }
117
- redis
81
+ @redis ||= Redis.new(:host=>self.host, :port=>self.port, :db=>self.db,
82
+ :password=>self.password, :logger=>self.logger)
83
+ class << self ; self ; end.send(:define_method, :redis) { @redis }
84
+ @redis
85
+ end
86
+
87
+ # Switches playground to use MockRedis instead of a live server.
88
+ # Particularly useful for testing, e.g. if you can't access Redis on your CI
89
+ # server. This method has no affect after playground accesses live Redis
90
+ # server.
91
+ #
92
+ # @example Put this in config/environments/test.rb
93
+ # config.after_initialize { Vanity.playground.mock! }
94
+ def mock!
95
+ @redis ||= MockRedis.new
118
96
  end
119
97
 
98
+ # Returns a metric (creating one if doesn't already exist).
99
+ #
100
+ # @since 1.1.0
101
+ def metric(id)
102
+ id = id.to_sym
103
+ metrics[id] ||= Metric.load(self, @loading, File.expand_path("metrics", load_path), id)
104
+ end
105
+
106
+ # Returns hash of metrics (key is metric id).
107
+ #
108
+ # @since 1.1.0
109
+ def metrics
110
+ unless @metrics
111
+ @metrics = {}
112
+ Dir[File.join(load_path, "metrics/*.rb")].each do |file|
113
+ begin
114
+ id = File.basename(file).gsub(/.rb$/, "")
115
+ metric id
116
+ rescue NameError
117
+ @logger.error "Could not load metric #{$!.name}: #{$!}"
118
+ end
119
+ end
120
+ end
121
+ @metrics
122
+ end
123
+
124
+ # Tracks an action associated with a metric.
125
+ #
126
+ # @example
127
+ # Vanity.playground.track! :uploaded_video
128
+ #
129
+ # @since 1.1.0
130
+ def track!(id, count = 1)
131
+ metric(id).track! count
132
+ end
120
133
  end
121
134
 
122
135
  @playground = Playground.new
123
136
  class << self
124
137
 
125
- # Returns the playground instance.
126
- def playground
127
- @playground
128
- end
138
+ # The playground instance.
139
+ #
140
+ # @see Vanity::Playground
141
+ attr_accessor :playground
129
142
 
130
143
  # Returns the Vanity context. For example, when using Rails this would be
131
144
  # the current controller, which can be used to get/set the vanity identity.
@@ -152,8 +165,12 @@ end
152
165
 
153
166
  class Object
154
167
 
155
- # Use this method to access an experiment by name. For example:
168
+ # Use this method to access an experiment by name.
169
+ #
170
+ # @example
156
171
  # puts experiment(:text_size).alternatives
172
+ #
173
+ # @see Vanity::Playground#experiment
157
174
  def experiment(name)
158
175
  Vanity.playground.experiment(name)
159
176
  end