mikeg-vanity 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +153 -0
- data/MIT-LICENSE +21 -0
- data/README.rdoc +83 -0
- data/bin/vanity +53 -0
- data/lib/vanity.rb +38 -0
- data/lib/vanity/backport.rb +43 -0
- data/lib/vanity/commands.rb +2 -0
- data/lib/vanity/commands/list.rb +21 -0
- data/lib/vanity/commands/report.rb +60 -0
- data/lib/vanity/experiment/ab_test.rb +477 -0
- data/lib/vanity/experiment/base.rb +212 -0
- data/lib/vanity/helpers.rb +59 -0
- data/lib/vanity/metric/active_record.rb +77 -0
- data/lib/vanity/metric/base.rb +221 -0
- data/lib/vanity/metric/google_analytics.rb +70 -0
- data/lib/vanity/mock_redis.rb +76 -0
- data/lib/vanity/playground.rb +197 -0
- data/lib/vanity/rails.rb +22 -0
- data/lib/vanity/rails/dashboard.rb +24 -0
- data/lib/vanity/rails/helpers.rb +158 -0
- data/lib/vanity/rails/testing.rb +11 -0
- data/lib/vanity/templates/_ab_test.erb +26 -0
- data/lib/vanity/templates/_experiment.erb +5 -0
- data/lib/vanity/templates/_experiments.erb +7 -0
- data/lib/vanity/templates/_metric.erb +14 -0
- data/lib/vanity/templates/_metrics.erb +13 -0
- data/lib/vanity/templates/_report.erb +27 -0
- data/lib/vanity/templates/flot.min.js +1 -0
- data/lib/vanity/templates/jquery.min.js +19 -0
- data/lib/vanity/templates/vanity.css +26 -0
- data/lib/vanity/templates/vanity.js +82 -0
- data/test/ab_test_test.rb +656 -0
- data/test/experiment_test.rb +136 -0
- data/test/experiments/age_and_zipcode.rb +19 -0
- data/test/experiments/metrics/cheers.rb +3 -0
- data/test/experiments/metrics/signups.rb +2 -0
- data/test/experiments/metrics/yawns.rb +3 -0
- data/test/experiments/null_abc.rb +5 -0
- data/test/metric_test.rb +518 -0
- data/test/playground_test.rb +10 -0
- data/test/rails_test.rb +104 -0
- data/test/test_helper.rb +135 -0
- data/vanity.gemspec +18 -0
- data/vendor/redis-rb/LICENSE +20 -0
- data/vendor/redis-rb/README.markdown +36 -0
- data/vendor/redis-rb/Rakefile +62 -0
- data/vendor/redis-rb/bench.rb +44 -0
- data/vendor/redis-rb/benchmarking/suite.rb +24 -0
- data/vendor/redis-rb/benchmarking/worker.rb +71 -0
- data/vendor/redis-rb/bin/distredis +33 -0
- data/vendor/redis-rb/examples/basic.rb +16 -0
- data/vendor/redis-rb/examples/incr-decr.rb +18 -0
- data/vendor/redis-rb/examples/list.rb +26 -0
- data/vendor/redis-rb/examples/sets.rb +36 -0
- data/vendor/redis-rb/lib/dist_redis.rb +124 -0
- data/vendor/redis-rb/lib/hash_ring.rb +128 -0
- data/vendor/redis-rb/lib/pipeline.rb +21 -0
- data/vendor/redis-rb/lib/redis.rb +370 -0
- data/vendor/redis-rb/lib/redis/raketasks.rb +1 -0
- data/vendor/redis-rb/profile.rb +22 -0
- data/vendor/redis-rb/redis-rb.gemspec +30 -0
- data/vendor/redis-rb/spec/redis_spec.rb +637 -0
- data/vendor/redis-rb/spec/spec_helper.rb +4 -0
- data/vendor/redis-rb/speed.rb +16 -0
- data/vendor/redis-rb/tasks/redis.tasks.rb +140 -0
- metadata +125 -0
@@ -0,0 +1,70 @@
|
|
1
|
+
module Vanity
|
2
|
+
class Metric
|
3
|
+
|
4
|
+
# Use Google Analytics metric.
|
5
|
+
#
|
6
|
+
# @example Page views
|
7
|
+
# metric "Page views" do
|
8
|
+
# google_analytics "UA-1828623-6"
|
9
|
+
# end
|
10
|
+
# @example Visits
|
11
|
+
# metric "Visits" do
|
12
|
+
# google_analytics "UA-1828623-6", :visits
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# @since 1.3.0
|
16
|
+
# @see Vanity::Metric::GoogleAnalytics
|
17
|
+
def google_analytics(web_property_id, *args)
|
18
|
+
gem "garb"
|
19
|
+
require "garb"
|
20
|
+
options = Hash === args.last ? args.pop : {}
|
21
|
+
metric = options.shift || :pageviews
|
22
|
+
@ga_resource = Vanity::Metric::GoogleAnalytics::Resource.new(web_property_id, metric)
|
23
|
+
@ga_mapper = options[:mapper] ||= lambda { |entry| entry.send(@ga_resource.metrics.elements.first).to_i }
|
24
|
+
extend GoogleAnalytics
|
25
|
+
rescue Gem::LoadError
|
26
|
+
fail LoadError, "Google Analytics metrics require Garb, please gem install garb first"
|
27
|
+
end
|
28
|
+
|
29
|
+
# Calling google_analytics method on a metric extends it with these modules,
|
30
|
+
# redefining the values and hook methods.
|
31
|
+
#
|
32
|
+
# @since 1.3.0
|
33
|
+
module GoogleAnalytics
|
34
|
+
|
35
|
+
# Returns values from GA using parameters specified by prior call to
|
36
|
+
# google_analytics.
|
37
|
+
def values(from, to)
|
38
|
+
data = @ga_resource.results(from, to).inject({}) do |hash,entry|
|
39
|
+
hash.merge(entry.date=>@ga_mapper.call(entry))
|
40
|
+
end
|
41
|
+
(from..to).map { |day| data[day.strftime('%Y%m%d')] || 0 }
|
42
|
+
end
|
43
|
+
|
44
|
+
# Hooks not supported for GA metrics.
|
45
|
+
def hook
|
46
|
+
fail "Cannot use hooks with Google Analytics methods"
|
47
|
+
end
|
48
|
+
|
49
|
+
class Resource
|
50
|
+
include Garb::Resource
|
51
|
+
|
52
|
+
def initialize(web_property_id, metric)
|
53
|
+
@web_property_id = web_property_id
|
54
|
+
metrics metric
|
55
|
+
dimensions :date
|
56
|
+
sort :date
|
57
|
+
end
|
58
|
+
|
59
|
+
def results(start_date, end_date)
|
60
|
+
@profile = Garb::Profile.all.find { |p| p.web_property_id == @web_property_id }
|
61
|
+
@start_date = start_date
|
62
|
+
@end_date = end_date
|
63
|
+
Garb::ReportResponse.new(send_request_for_body).results
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|
70
|
+
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
|
@@ -0,0 +1,197 @@
|
|
1
|
+
module Vanity
|
2
|
+
|
3
|
+
# Playground catalogs all your experiments, holds the Vanity configuration.
|
4
|
+
#
|
5
|
+
# @example
|
6
|
+
# Vanity.playground.logger = my_logger
|
7
|
+
# puts Vanity.playground.map(&:name)
|
8
|
+
class Playground
|
9
|
+
|
10
|
+
DEFAULTS = { :host=>"127.0.0.1", :port=>6379, :db=>0, :load_path=>"experiments" }
|
11
|
+
|
12
|
+
# Created new Playground. Unless you need to, use the global Vanity.playground.
|
13
|
+
def initialize(options = {})
|
14
|
+
@host, @port, @db, @load_path = DEFAULTS.merge(options).values_at(:host, :port, :db, :load_path)
|
15
|
+
@namespace = "vanity:#{Vanity::Version::MAJOR}"
|
16
|
+
@logger = options[:logger]
|
17
|
+
unless @logger
|
18
|
+
@logger = Logger.new(STDOUT)
|
19
|
+
@logger.level = Logger::ERROR
|
20
|
+
end
|
21
|
+
@redis = options[:redis]
|
22
|
+
@loading = []
|
23
|
+
end
|
24
|
+
|
25
|
+
# Redis host name. Default is 127.0.0.1
|
26
|
+
attr_accessor :host
|
27
|
+
|
28
|
+
# Redis port number. Default is 6379.
|
29
|
+
attr_accessor :port
|
30
|
+
|
31
|
+
# Redis database number. Default is 0.
|
32
|
+
attr_accessor :db
|
33
|
+
|
34
|
+
# Redis database password.
|
35
|
+
attr_accessor :password
|
36
|
+
|
37
|
+
# Namespace for database keys. Default is vanity:n, where n is the major release number, e.g. vanity:1 for 1.0.3.
|
38
|
+
attr_accessor :namespace
|
39
|
+
|
40
|
+
# Path to load experiment files from.
|
41
|
+
attr_accessor :load_path
|
42
|
+
|
43
|
+
# Logger.
|
44
|
+
attr_accessor :logger
|
45
|
+
|
46
|
+
# Defines a new experiment. Generally, do not call this directly,
|
47
|
+
# use one of the definition methods (ab_test, measure, etc).
|
48
|
+
#
|
49
|
+
# @see Vanity::Experiment
|
50
|
+
def define(name, type, options = {}, &block)
|
51
|
+
warn "Deprecated: if you need this functionality let's make a better API"
|
52
|
+
id = name.to_s.downcase.gsub(/\W/, "_").to_sym
|
53
|
+
raise "Experiment #{id} already defined once" if experiments[id]
|
54
|
+
klass = Experiment.const_get(type.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase })
|
55
|
+
experiment = klass.new(self, id, name, options)
|
56
|
+
experiment.instance_eval &block
|
57
|
+
experiment.save
|
58
|
+
experiments[id] = experiment
|
59
|
+
end
|
60
|
+
|
61
|
+
# Returns the experiment. You may not have guessed, but this method raises
|
62
|
+
# an exception if it cannot load the experiment's definition.
|
63
|
+
#
|
64
|
+
# @see Vanity::Experiment
|
65
|
+
def experiment(name)
|
66
|
+
id = name.to_s.downcase.gsub(/\W/, "_").to_sym
|
67
|
+
warn "Deprecated: pleae call experiment method with experiment identifier (a Ruby symbol)" unless id == name
|
68
|
+
experiments[id.to_sym] or raise NameError, "No experiment #{id}"
|
69
|
+
end
|
70
|
+
|
71
|
+
# Returns hash of experiments (key is experiment id).
|
72
|
+
#
|
73
|
+
# @see Vanity::Experiment
|
74
|
+
def experiments
|
75
|
+
unless @experiments
|
76
|
+
@experiments = {}
|
77
|
+
@logger.info "Vanity: loading experiments from #{load_path}"
|
78
|
+
Dir[File.join(load_path, "*.rb")].each do |file|
|
79
|
+
Experiment::Base.load self, @loading, file
|
80
|
+
end
|
81
|
+
end
|
82
|
+
@experiments
|
83
|
+
end
|
84
|
+
|
85
|
+
# Reloads all metrics and experiments. Rails calls this for each request in
|
86
|
+
# development mode.
|
87
|
+
def reload!
|
88
|
+
@experiments = nil
|
89
|
+
@metrics = nil
|
90
|
+
load!
|
91
|
+
end
|
92
|
+
|
93
|
+
# Loads all metrics and experiments. Rails calls this during
|
94
|
+
# initialization.
|
95
|
+
def load!
|
96
|
+
experiments
|
97
|
+
metrics
|
98
|
+
end
|
99
|
+
|
100
|
+
# Use this instance to access the Redis database.
|
101
|
+
def redis
|
102
|
+
@redis ||= Redis.new(:host=>self.host, :port=>self.port, :db=>self.db,
|
103
|
+
:password=>self.password, :logger=>self.logger)
|
104
|
+
class << self ; self ; end.send(:define_method, :redis) { @redis }
|
105
|
+
@redis
|
106
|
+
end
|
107
|
+
|
108
|
+
# Switches playground to use MockRedis instead of a live server.
|
109
|
+
# Particularly useful for testing, e.g. if you can't access Redis on your CI
|
110
|
+
# server. This method has no affect after playground accesses live Redis
|
111
|
+
# server.
|
112
|
+
#
|
113
|
+
# @example Put this in config/environments/test.rb
|
114
|
+
# config.after_initialize { Vanity.playground.mock! }
|
115
|
+
def mock!
|
116
|
+
@redis ||= MockRedis.new
|
117
|
+
end
|
118
|
+
|
119
|
+
# Returns a metric (raises NameError if no metric with that identifier).
|
120
|
+
#
|
121
|
+
# @see Vanity::Metric
|
122
|
+
# @since 1.1.0
|
123
|
+
def metric(id)
|
124
|
+
metrics[id.to_sym] or raise NameError, "No metric #{id}"
|
125
|
+
end
|
126
|
+
|
127
|
+
# Returns hash of metrics (key is metric id).
|
128
|
+
#
|
129
|
+
# @see Vanity::Metric
|
130
|
+
# @since 1.1.0
|
131
|
+
def metrics
|
132
|
+
unless @metrics
|
133
|
+
@metrics = {}
|
134
|
+
@logger.info "Vanity: loading metrics from #{load_path}/metrics"
|
135
|
+
Dir[File.join(load_path, "metrics/*.rb")].each do |file|
|
136
|
+
Metric.load self, @loading, file
|
137
|
+
end
|
138
|
+
end
|
139
|
+
@metrics
|
140
|
+
end
|
141
|
+
|
142
|
+
# Tracks an action associated with a metric.
|
143
|
+
#
|
144
|
+
# @example
|
145
|
+
# Vanity.playground.track! :uploaded_video
|
146
|
+
#
|
147
|
+
# @since 1.1.0
|
148
|
+
def track!(id, count = 1)
|
149
|
+
metric(id).track! count
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
@playground = Playground.new
|
154
|
+
class << self
|
155
|
+
|
156
|
+
# The playground instance.
|
157
|
+
#
|
158
|
+
# @see Vanity::Playground
|
159
|
+
attr_accessor :playground
|
160
|
+
|
161
|
+
# Returns the Vanity context. For example, when using Rails this would be
|
162
|
+
# the current controller, which can be used to get/set the vanity identity.
|
163
|
+
def context
|
164
|
+
Thread.current[:vanity_context]
|
165
|
+
end
|
166
|
+
|
167
|
+
# Sets the Vanity context. For example, when using Rails this would be
|
168
|
+
# set by the set_vanity_context before filter (via Vanity::Rails#use_vanity).
|
169
|
+
def context=(context)
|
170
|
+
Thread.current[:vanity_context] = context
|
171
|
+
end
|
172
|
+
|
173
|
+
# Path to template.
|
174
|
+
def template(name)
|
175
|
+
path = File.join(File.dirname(__FILE__), "templates/#{name}")
|
176
|
+
path << ".erb" unless name["."]
|
177
|
+
path
|
178
|
+
end
|
179
|
+
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
|
184
|
+
class Object
|
185
|
+
|
186
|
+
# Use this method to access an experiment by name.
|
187
|
+
#
|
188
|
+
# @example
|
189
|
+
# puts experiment(:text_size).alternatives
|
190
|
+
#
|
191
|
+
# @see Vanity::Playground#experiment
|
192
|
+
# @deprecated
|
193
|
+
def experiment(name)
|
194
|
+
warn "Deprecated. Please call Vanity.playground.experiment directly."
|
195
|
+
Vanity.playground.experiment(name)
|
196
|
+
end
|
197
|
+
end
|
data/lib/vanity/rails.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require "vanity/rails/helpers"
|
2
|
+
require "vanity/rails/testing"
|
3
|
+
require "vanity/rails/dashboard"
|
4
|
+
|
5
|
+
# Include in controller, add view helper methods.
|
6
|
+
ActionController::Base.class_eval do
|
7
|
+
extend Vanity::Rails::UseVanity
|
8
|
+
include Vanity::Rails::Filters
|
9
|
+
helper Vanity::Rails::Helpers
|
10
|
+
end
|
11
|
+
|
12
|
+
Rails.configuration.after_initialize do
|
13
|
+
# Use Rails logger by default.
|
14
|
+
Vanity.playground.logger ||= ActionController::Base.logger
|
15
|
+
Vanity.playground.load_path = "#{RAILS_ROOT}/experiments"
|
16
|
+
|
17
|
+
# Do this at the very end of initialization, allowing test environment to do
|
18
|
+
# Vanity.playground.mock! before any database access takes place.
|
19
|
+
Rails.configuration.after_initialize do
|
20
|
+
Vanity.playground.load!
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Vanity
|
2
|
+
module Rails
|
3
|
+
# Step 1: Add a new resource in config/routes.rb:
|
4
|
+
# map.vanity "/vanity/:action/:id", :controller=>:vanity
|
5
|
+
#
|
6
|
+
# Step 2: Create a new experiments controller:
|
7
|
+
# class VanityController < ApplicationController
|
8
|
+
# include Vanity::Rails::Dashboard
|
9
|
+
# end
|
10
|
+
#
|
11
|
+
# Step 3: Open your browser to http://localhost:3000/vanity
|
12
|
+
module Dashboard
|
13
|
+
def index
|
14
|
+
render Vanity.template("_report"), :content_type=>Mime::HTML, :layout=>true
|
15
|
+
end
|
16
|
+
|
17
|
+
def chooses
|
18
|
+
exp = Vanity.playground.experiment(params[:e])
|
19
|
+
exp.chooses(exp.alternatives[params[:a].to_i].value)
|
20
|
+
render :partial=>Vanity.template("experiment"), :locals=>{ :experiment=>exp }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
module Vanity
|
2
|
+
# Helper methods for use in your controllers.
|
3
|
+
#
|
4
|
+
# 1) Use Vanity from within your controller:
|
5
|
+
#
|
6
|
+
# class ApplicationController < ActionController::Base
|
7
|
+
# use_vanity :current_user end
|
8
|
+
# end
|
9
|
+
#
|
10
|
+
# 2) Present different options for an A/B test:
|
11
|
+
#
|
12
|
+
# Get started for only $<%= ab_test :pricing %> a month!
|
13
|
+
#
|
14
|
+
# 3) Measure conversion:
|
15
|
+
#
|
16
|
+
# def signup
|
17
|
+
# track! :pricing
|
18
|
+
# . . .
|
19
|
+
# end
|
20
|
+
module Rails
|
21
|
+
module UseVanity
|
22
|
+
|
23
|
+
protected
|
24
|
+
|
25
|
+
# Defines the vanity_identity method and the set_identity_context filter.
|
26
|
+
#
|
27
|
+
# Call with the name of a method that returns an object whose identity
|
28
|
+
# will be used as the Vanity identity. Confusing? Let's try by example:
|
29
|
+
#
|
30
|
+
# class ApplicationController < ActionController::Base
|
31
|
+
# use_vanity :current_user
|
32
|
+
#
|
33
|
+
# def current_user
|
34
|
+
# User.find(session[:user_id])
|
35
|
+
# end
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
# If that method (current_user in this example) returns nil, Vanity will
|
39
|
+
# set the identity for you (using a cookie to remember it across
|
40
|
+
# requests). It also uses this mechanism if you don't provide an
|
41
|
+
# identity object, by calling use_vanity with no arguments.
|
42
|
+
#
|
43
|
+
# Of course you can also use a block:
|
44
|
+
# class ProjectController < ApplicationController
|
45
|
+
# use_vanity { |controller| controller.params[:project_id] }
|
46
|
+
# end
|
47
|
+
def use_vanity(symbol = nil, &block)
|
48
|
+
if block
|
49
|
+
define_method(:vanity_identity) { block.call(self) }
|
50
|
+
else
|
51
|
+
define_method :vanity_identity do
|
52
|
+
return @vanity_identity if @vanity_identity
|
53
|
+
if symbol && object = send(symbol)
|
54
|
+
@vanity_identity = object.id
|
55
|
+
elsif response # everyday use
|
56
|
+
@vanity_identity = cookies["vanity_id"] || ActiveSupport::SecureRandom.hex(16)
|
57
|
+
cookies["vanity_id"] = { :value=>@vanity_identity, :expires=>1.month.from_now }
|
58
|
+
@vanity_identity
|
59
|
+
else # during functional testing
|
60
|
+
@vanity_identity = "test"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
around_filter :vanity_context_filter
|
65
|
+
before_filter :vanity_reload_filter unless ::Rails.configuration.cache_classes
|
66
|
+
before_filter :vanity_query_parameter_filter
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
module Filters
|
72
|
+
protected
|
73
|
+
|
74
|
+
# Around filter that sets Vanity.context to controller.
|
75
|
+
def vanity_context_filter
|
76
|
+
previous, Vanity.context = Vanity.context, self
|
77
|
+
yield
|
78
|
+
ensure
|
79
|
+
Vanity.context = previous
|
80
|
+
end
|
81
|
+
|
82
|
+
# This filter allows user to choose alternative in experiment using query
|
83
|
+
# parameter.
|
84
|
+
#
|
85
|
+
# Each alternative has a unique fingerprint (run vanity list command to
|
86
|
+
# see them all). A request with the _vanity query parameter is
|
87
|
+
# intercepted, the alternative is chosen, and the user redirected to the
|
88
|
+
# same request URL sans _vanity parameter. This only works for GET
|
89
|
+
# requests.
|
90
|
+
#
|
91
|
+
# For example, if the user requests the page
|
92
|
+
# http://example.com/?_vanity=2907dac4de, the first alternative of the
|
93
|
+
# :null_abc experiment is chosen and the user redirected to
|
94
|
+
# http://example.com/.
|
95
|
+
def vanity_query_parameter_filter
|
96
|
+
if request.get? && params[:_vanity]
|
97
|
+
hashes = Array(params.delete(:_vanity))
|
98
|
+
Vanity.playground.experiments.each do |id, experiment|
|
99
|
+
if experiment.respond_to?(:alternatives)
|
100
|
+
experiment.alternatives.each do |alt|
|
101
|
+
if hash = hashes.delete(experiment.fingerprint(alt))
|
102
|
+
experiment.chooses alt.value
|
103
|
+
break
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
break if hashes.empty?
|
108
|
+
end
|
109
|
+
redirect_to url_for(params)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# Before filter to reload Vanity experiments/metrics. Enabled when
|
114
|
+
# cache_classes is false (typically, testing environment).
|
115
|
+
def vanity_reload_filter
|
116
|
+
Vanity.playground.reload!
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
|
121
|
+
module Helpers
|
122
|
+
|
123
|
+
# This method returns one of the alternative values in the named A/B test.
|
124
|
+
#
|
125
|
+
# @example A/B two alternatives for a page
|
126
|
+
# def index
|
127
|
+
# if ab_test(:new_page) # true/false test
|
128
|
+
# render action: "new_page"
|
129
|
+
# else
|
130
|
+
# render action: "index"
|
131
|
+
# end
|
132
|
+
# end
|
133
|
+
# @example Similar, alternative value is page name
|
134
|
+
# def index
|
135
|
+
# render action: ab_test(:new_page)
|
136
|
+
# end
|
137
|
+
# @example A/B test inside ERB template (condition)
|
138
|
+
# <%= if ab_test(:banner) %>100% less complexity!<% end %>
|
139
|
+
# @example A/B test inside ERB template (value)
|
140
|
+
# <%= ab_test(:greeting) %> <%= current_user.name %>
|
141
|
+
# @example A/B test inside ERB template (capture)
|
142
|
+
# <% ab_test :features do |count| %>
|
143
|
+
# <%= count %> features to choose from!
|
144
|
+
# <% end %>
|
145
|
+
def ab_test(name, &block)
|
146
|
+
value = Vanity.playground.experiment(name).choose
|
147
|
+
if block
|
148
|
+
content = capture(value, &block)
|
149
|
+
block_called_from_erb?(block) ? concat(content) : content
|
150
|
+
else
|
151
|
+
value
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
end
|
156
|
+
|
157
|
+
end
|
158
|
+
end
|