mikeg-vanity 1.3.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.
- 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
|