moses-vanity 1.7.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.autotest +22 -0
- data/.gitignore +7 -0
- data/.rvmrc +3 -0
- data/.travis.yml +13 -0
- data/CHANGELOG +374 -0
- data/Gemfile +28 -0
- data/MIT-LICENSE +21 -0
- data/README.rdoc +108 -0
- data/Rakefile +189 -0
- data/bin/vanity +16 -0
- data/doc/_config.yml +2 -0
- data/doc/_layouts/_header.html +34 -0
- data/doc/_layouts/page.html +47 -0
- data/doc/_metrics.textile +12 -0
- data/doc/ab_testing.textile +210 -0
- data/doc/configuring.textile +45 -0
- data/doc/contributing.textile +93 -0
- data/doc/credits.textile +23 -0
- data/doc/css/page.css +83 -0
- data/doc/css/print.css +43 -0
- data/doc/css/syntax.css +7 -0
- data/doc/email.textile +129 -0
- data/doc/experimental.textile +31 -0
- data/doc/faq.textile +8 -0
- data/doc/identity.textile +43 -0
- data/doc/images/ab_in_dashboard.png +0 -0
- data/doc/images/clear_winner.png +0 -0
- data/doc/images/price_options.png +0 -0
- data/doc/images/sidebar_test.png +0 -0
- data/doc/images/signup_metric.png +0 -0
- data/doc/images/vanity.png +0 -0
- data/doc/index.textile +91 -0
- data/doc/metrics.textile +231 -0
- data/doc/rails.textile +89 -0
- data/doc/site.js +27 -0
- data/generators/templates/vanity_migration.rb +53 -0
- data/generators/vanity_generator.rb +8 -0
- data/lib/generators/templates/vanity_migration.rb +53 -0
- data/lib/generators/vanity_generator.rb +15 -0
- data/lib/vanity.rb +36 -0
- data/lib/vanity/adapters/abstract_adapter.rb +140 -0
- data/lib/vanity/adapters/active_record_adapter.rb +248 -0
- data/lib/vanity/adapters/mock_adapter.rb +157 -0
- data/lib/vanity/adapters/mongodb_adapter.rb +178 -0
- data/lib/vanity/adapters/redis_adapter.rb +160 -0
- data/lib/vanity/backport.rb +26 -0
- data/lib/vanity/commands/list.rb +21 -0
- data/lib/vanity/commands/report.rb +64 -0
- data/lib/vanity/commands/upgrade.rb +34 -0
- data/lib/vanity/experiment/ab_test.rb +507 -0
- data/lib/vanity/experiment/base.rb +214 -0
- data/lib/vanity/frameworks.rb +16 -0
- data/lib/vanity/frameworks/rails.rb +318 -0
- data/lib/vanity/helpers.rb +66 -0
- data/lib/vanity/images/x.gif +0 -0
- data/lib/vanity/metric/active_record.rb +85 -0
- data/lib/vanity/metric/base.rb +244 -0
- data/lib/vanity/metric/google_analytics.rb +83 -0
- data/lib/vanity/metric/remote.rb +53 -0
- data/lib/vanity/playground.rb +396 -0
- data/lib/vanity/templates/_ab_test.erb +28 -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/_vanity.js.erb +20 -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/lib/vanity/version.rb +11 -0
- data/test/adapters/redis_adapter_test.rb +17 -0
- data/test/experiment/ab_test.rb +771 -0
- data/test/experiment/base_test.rb +150 -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/active_record_test.rb +277 -0
- data/test/metric/base_test.rb +293 -0
- data/test/metric/google_analytics_test.rb +104 -0
- data/test/metric/remote_test.rb +109 -0
- data/test/myapp/app/controllers/application_controller.rb +2 -0
- data/test/myapp/app/controllers/main_controller.rb +7 -0
- data/test/myapp/config/boot.rb +110 -0
- data/test/myapp/config/environment.rb +10 -0
- data/test/myapp/config/environments/production.rb +0 -0
- data/test/myapp/config/routes.rb +3 -0
- data/test/passenger_test.rb +43 -0
- data/test/playground_test.rb +26 -0
- data/test/rails_dashboard_test.rb +37 -0
- data/test/rails_helper_test.rb +36 -0
- data/test/rails_test.rb +389 -0
- data/test/test_helper.rb +145 -0
- data/vanity.gemspec +26 -0
- metadata +202 -0
@@ -0,0 +1,214 @@
|
|
1
|
+
module Vanity
|
2
|
+
module Experiment
|
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
|
+
attr_reader :playground
|
14
|
+
|
15
|
+
# Defines a new experiment, given the experiment's name, type and
|
16
|
+
# definition block.
|
17
|
+
def define(name, type, options = nil, &block)
|
18
|
+
fail "Experiment #{@experiment_id} already defined in playground" if playground.experiments[@experiment_id]
|
19
|
+
klass = Experiment.const_get(type.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase })
|
20
|
+
experiment = klass.new(playground, @experiment_id, name, options)
|
21
|
+
experiment.instance_eval &block
|
22
|
+
experiment.save
|
23
|
+
playground.experiments[@experiment_id] = experiment
|
24
|
+
end
|
25
|
+
|
26
|
+
def new_binding(playground, id)
|
27
|
+
@playground, @experiment_id = playground, id
|
28
|
+
binding
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
# Base class that all experiment types are derived from.
|
34
|
+
class Base
|
35
|
+
|
36
|
+
class << self
|
37
|
+
|
38
|
+
# Returns the type of this class as a symbol (e.g. AbTest becomes
|
39
|
+
# ab_test).
|
40
|
+
def type
|
41
|
+
name.split("::").last.gsub(/([a-z])([A-Z])/) { "#{$1}_#{$2}" }.gsub(/([A-Z])([A-Z][a-z])/) { "#{$1}_#{$2}" }.downcase
|
42
|
+
end
|
43
|
+
|
44
|
+
# Playground uses this to load experiment definitions.
|
45
|
+
def load(playground, stack, file)
|
46
|
+
fail "Circular dependency detected: #{stack.join('=>')}=>#{file}" if stack.include?(file)
|
47
|
+
source = File.read(file)
|
48
|
+
stack.push file
|
49
|
+
id = File.basename(file, ".rb").downcase.gsub(/\W/, "_").to_sym
|
50
|
+
context = Object.new
|
51
|
+
context.instance_eval do
|
52
|
+
extend Definition
|
53
|
+
experiment = eval(source, context.new_binding(playground, id), file)
|
54
|
+
fail NameError.new("Expected #{file} to define experiment #{id}", id) unless playground.experiments[id]
|
55
|
+
return experiment
|
56
|
+
end
|
57
|
+
rescue
|
58
|
+
error = NameError.exception($!.message, id)
|
59
|
+
error.set_backtrace $!.backtrace
|
60
|
+
raise error
|
61
|
+
ensure
|
62
|
+
stack.pop
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
def initialize(playground, id, name, options = nil)
|
68
|
+
@playground = playground
|
69
|
+
@id, @name = id.to_sym, name
|
70
|
+
@options = options || {}
|
71
|
+
@identify_block = method(:default_identify)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Human readable experiment name (first argument you pass when creating a
|
75
|
+
# new experiment).
|
76
|
+
attr_reader :name
|
77
|
+
alias :to_s :name
|
78
|
+
|
79
|
+
# Unique identifier, derived from name experiment name, e.g. "Green
|
80
|
+
# Button" becomes :green_button.
|
81
|
+
attr_reader :id
|
82
|
+
|
83
|
+
attr_reader :playground
|
84
|
+
|
85
|
+
# Time stamp when experiment was created.
|
86
|
+
def created_at
|
87
|
+
@created_at ||= connection.get_experiment_created_at(@id)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Time stamp when experiment was completed.
|
91
|
+
attr_reader :completed_at
|
92
|
+
|
93
|
+
# Returns the type of this experiment as a symbol (e.g. :ab_test).
|
94
|
+
def type
|
95
|
+
self.class.type
|
96
|
+
end
|
97
|
+
|
98
|
+
# Defines how we obtain an identity for the current experiment. Usually
|
99
|
+
# Vanity gets the identity form a session object (see use_vanity), but
|
100
|
+
# there are cases where you want a particular experiment to use a
|
101
|
+
# different identity.
|
102
|
+
#
|
103
|
+
# For example, if all your experiments use current_user and you need one
|
104
|
+
# experiment to use the current project:
|
105
|
+
# ab_test "Project widget" do
|
106
|
+
# alternatives :small, :medium, :large
|
107
|
+
# identify do |controller|
|
108
|
+
# controller.project.id
|
109
|
+
# end
|
110
|
+
# end
|
111
|
+
def identify(&block)
|
112
|
+
fail "Missing block" unless block
|
113
|
+
@identify_block = block
|
114
|
+
end
|
115
|
+
|
116
|
+
|
117
|
+
# -- Reporting --
|
118
|
+
|
119
|
+
# Sets or returns description. For example
|
120
|
+
# ab_test "Simple" do
|
121
|
+
# description "A simple A/B experiment"
|
122
|
+
# end
|
123
|
+
#
|
124
|
+
# puts "Just defined: " + experiment(:simple).description
|
125
|
+
def description(text = nil)
|
126
|
+
@description = text if text
|
127
|
+
@description
|
128
|
+
end
|
129
|
+
|
130
|
+
|
131
|
+
# -- Experiment completion --
|
132
|
+
|
133
|
+
# Define experiment completion condition. For example:
|
134
|
+
# complete_if do
|
135
|
+
# !score(95).chosen.nil?
|
136
|
+
# end
|
137
|
+
def complete_if(&block)
|
138
|
+
raise ArgumentError, "Missing block" unless block
|
139
|
+
raise "complete_if already called on this experiment" if @complete_block
|
140
|
+
@complete_block = block
|
141
|
+
end
|
142
|
+
|
143
|
+
# Force experiment to complete.
|
144
|
+
def complete!
|
145
|
+
@playground.logger.info "vanity: completed experiment #{id}"
|
146
|
+
return unless @playground.collecting?
|
147
|
+
connection.set_experiment_completed_at @id, Time.now
|
148
|
+
@completed_at = connection.get_experiment_completed_at(@id)
|
149
|
+
end
|
150
|
+
|
151
|
+
# Time stamp when experiment was completed.
|
152
|
+
def completed_at
|
153
|
+
@completed_at ||= connection.get_experiment_completed_at(@id)
|
154
|
+
end
|
155
|
+
|
156
|
+
# Returns true if experiment active, false if completed.
|
157
|
+
def active?
|
158
|
+
!@playground.collecting? || !connection.is_experiment_completed?(@id)
|
159
|
+
end
|
160
|
+
|
161
|
+
# -- Store/validate --
|
162
|
+
|
163
|
+
# Get rid of all experiment data.
|
164
|
+
def destroy
|
165
|
+
connection.destroy_experiment @id
|
166
|
+
@created_at = @completed_at = nil
|
167
|
+
end
|
168
|
+
|
169
|
+
# Called by Playground to save the experiment definition.
|
170
|
+
def save
|
171
|
+
return unless @playground.collecting?
|
172
|
+
connection.set_experiment_created_at @id, Time.now
|
173
|
+
end
|
174
|
+
|
175
|
+
protected
|
176
|
+
|
177
|
+
def identity
|
178
|
+
@identify_block.call(Vanity.context)
|
179
|
+
end
|
180
|
+
|
181
|
+
def default_identify(context)
|
182
|
+
raise "No Vanity.context" unless context
|
183
|
+
raise "Vanity.context does not respond to vanity_identity" unless context.respond_to?(:vanity_identity)
|
184
|
+
context.send(:vanity_identity) or raise "Vanity.context.vanity_identity - no identity"
|
185
|
+
end
|
186
|
+
|
187
|
+
# Derived classes call this after state changes that may lead to
|
188
|
+
# experiment completing.
|
189
|
+
def check_completion!
|
190
|
+
if @complete_block
|
191
|
+
begin
|
192
|
+
complete! if @complete_block.call
|
193
|
+
rescue
|
194
|
+
warn "Error in Vanity::Experiment::Base: #{$!}"
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
# Returns key for this experiment, or with an argument, return a key
|
200
|
+
# using the experiment as the namespace. Examples:
|
201
|
+
# key => "vanity:experiments:green_button"
|
202
|
+
# key("participants") => "vanity:experiments:green_button:participants"
|
203
|
+
def key(name = nil)
|
204
|
+
"#{@id}:#{name}"
|
205
|
+
end
|
206
|
+
|
207
|
+
# Shortcut for Vanity.playground.connection
|
208
|
+
def connection
|
209
|
+
@playground.connection
|
210
|
+
end
|
211
|
+
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# Automatically configure Vanity.
|
2
|
+
if defined?(Rails)
|
3
|
+
if Rails.const_defined?(:Railtie) # Rails 3
|
4
|
+
class Plugin < Rails::Railtie # :nodoc:
|
5
|
+
initializer "vanity.require" do |app|
|
6
|
+
require 'vanity/frameworks/rails'
|
7
|
+
Vanity::Rails.load!
|
8
|
+
end
|
9
|
+
end
|
10
|
+
else
|
11
|
+
Rails.configuration.after_initialize do
|
12
|
+
require 'vanity/frameworks/rails'
|
13
|
+
Vanity::Rails.load!
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,318 @@
|
|
1
|
+
module Vanity
|
2
|
+
module Rails #:nodoc:
|
3
|
+
def self.load!
|
4
|
+
Vanity.playground.load_path = ::Rails.root + Vanity.playground.load_path
|
5
|
+
Vanity.playground.logger ||= ::Rails.logger
|
6
|
+
|
7
|
+
# Do this at the very end of initialization, allowing you to change
|
8
|
+
# connection adapter, turn collection on/off, etc.
|
9
|
+
::Rails.configuration.after_initialize do
|
10
|
+
Vanity.playground.load!
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# The use_vanity method will setup the controller to allow testing and
|
15
|
+
# tracking of the current user.
|
16
|
+
module UseVanity
|
17
|
+
# Defines the vanity_identity method and the set_identity_context filter.
|
18
|
+
#
|
19
|
+
# Call with the name of a method that returns an object whose identity
|
20
|
+
# will be used as the Vanity identity. Confusing? Let's try by example:
|
21
|
+
#
|
22
|
+
# class ApplicationController < ActionController::Base
|
23
|
+
# use_vanity :current_user
|
24
|
+
#
|
25
|
+
# def current_user
|
26
|
+
# User.find(session[:user_id])
|
27
|
+
# end
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# If that method (current_user in this example) returns nil, Vanity will
|
31
|
+
# set the identity for you (using a cookie to remember it across
|
32
|
+
# requests). It also uses this mechanism if you don't provide an
|
33
|
+
# identity object, by calling use_vanity with no arguments.
|
34
|
+
#
|
35
|
+
# Of course you can also use a block:
|
36
|
+
# class ProjectController < ApplicationController
|
37
|
+
# use_vanity { |controller| controller.params[:project_id] }
|
38
|
+
# end
|
39
|
+
def use_vanity(symbol = nil, &block)
|
40
|
+
if block
|
41
|
+
define_method(:vanity_identity) { block.call(self) }
|
42
|
+
else
|
43
|
+
define_method :vanity_identity do
|
44
|
+
return @vanity_identity if @vanity_identity
|
45
|
+
if symbol && object = send(symbol)
|
46
|
+
@vanity_identity = object.id
|
47
|
+
elsif request.get? && params[:_identity]
|
48
|
+
@vanity_identity = params[:_identity]
|
49
|
+
cookies["vanity_id"] = { :value=>@vanity_identity, :expires=>1.month.from_now }
|
50
|
+
@vanity_identity
|
51
|
+
elsif response # everyday use
|
52
|
+
@vanity_identity = cookies["vanity_id"] || ActiveSupport::SecureRandom.hex(16)
|
53
|
+
cookie = { :value=>@vanity_identity, :expires=>1.month.from_now }
|
54
|
+
# Useful if application and admin console are on separate domains.
|
55
|
+
# This only works in Rails 3.x.
|
56
|
+
cookie[:domain] ||= ::Rails.application.config.session_options[:domain] if ::Rails.respond_to?(:application)
|
57
|
+
cookies["vanity_id"] = cookie
|
58
|
+
@vanity_identity
|
59
|
+
else # during functional testing
|
60
|
+
@vanity_identity = "test"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
protected :vanity_identity
|
65
|
+
around_filter :vanity_context_filter
|
66
|
+
before_filter :vanity_reload_filter unless ::Rails.configuration.cache_classes
|
67
|
+
before_filter :vanity_query_parameter_filter
|
68
|
+
after_filter :vanity_track_filter
|
69
|
+
end
|
70
|
+
protected :use_vanity
|
71
|
+
end
|
72
|
+
|
73
|
+
module UseVanityMailer
|
74
|
+
def use_vanity_mailer(symbol = nil)
|
75
|
+
# Context is the instance of ActionMailer::Base
|
76
|
+
Vanity.context = self
|
77
|
+
if symbol && (@object = symbol)
|
78
|
+
class << self
|
79
|
+
define_method :vanity_identity do
|
80
|
+
@vanity_identity = (String === @object ? @object : @object.id)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
else
|
84
|
+
class << self
|
85
|
+
define_method :vanity_identity do
|
86
|
+
@vanity_identity = @vanity_identity || ActiveSupport::SecureRandom.hex(16)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
protected :use_vanity_mailer
|
92
|
+
end
|
93
|
+
|
94
|
+
|
95
|
+
# Vanity needs these filters. They are includes in ActionController and
|
96
|
+
# automatically added when you use #use_vanity in your controller.
|
97
|
+
module Filters
|
98
|
+
# Around filter that sets Vanity.context to controller.
|
99
|
+
def vanity_context_filter
|
100
|
+
previous, Vanity.context = Vanity.context, self
|
101
|
+
yield
|
102
|
+
ensure
|
103
|
+
Vanity.context = previous
|
104
|
+
end
|
105
|
+
|
106
|
+
# This filter allows user to choose alternative in experiment using query
|
107
|
+
# parameter.
|
108
|
+
#
|
109
|
+
# Each alternative has a unique fingerprint (run vanity list command to
|
110
|
+
# see them all). A request with the _vanity query parameter is
|
111
|
+
# intercepted, the alternative is chosen, and the user redirected to the
|
112
|
+
# same request URL sans _vanity parameter. This only works for GET
|
113
|
+
# requests.
|
114
|
+
#
|
115
|
+
# For example, if the user requests the page
|
116
|
+
# http://example.com/?_vanity=2907dac4de, the first alternative of the
|
117
|
+
# :null_abc experiment is chosen and the user redirected to
|
118
|
+
# http://example.com/.
|
119
|
+
def vanity_query_parameter_filter
|
120
|
+
if request.get? && params[:_vanity]
|
121
|
+
hashes = Array(params.delete(:_vanity))
|
122
|
+
Vanity.playground.experiments.each do |id, experiment|
|
123
|
+
if experiment.respond_to?(:alternatives)
|
124
|
+
experiment.alternatives.each do |alt|
|
125
|
+
if hash = hashes.delete(experiment.fingerprint(alt))
|
126
|
+
experiment.chooses alt.value
|
127
|
+
break
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
break if hashes.empty?
|
132
|
+
end
|
133
|
+
redirect_to url_for(params)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# Before filter to reload Vanity experiments/metrics. Enabled when
|
138
|
+
# cache_classes is false (typically, testing environment).
|
139
|
+
def vanity_reload_filter
|
140
|
+
Vanity.playground.reload!
|
141
|
+
end
|
142
|
+
|
143
|
+
# Filter to track metrics
|
144
|
+
# pass _track param along to call track! on that alternative
|
145
|
+
def vanity_track_filter
|
146
|
+
if request.get? && params[:_track]
|
147
|
+
track! params[:_track]
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
protected :vanity_context_filter, :vanity_query_parameter_filter, :vanity_reload_filter
|
152
|
+
end
|
153
|
+
|
154
|
+
|
155
|
+
# Introduces ab_test helper (controllers and views). Similar to the generic
|
156
|
+
# ab_test method, with the ability to capture content (applicable to views,
|
157
|
+
# see examples).
|
158
|
+
module Helpers
|
159
|
+
# This method returns one of the alternative values in the named A/B test.
|
160
|
+
#
|
161
|
+
# @example A/B two alternatives for a page
|
162
|
+
# def index
|
163
|
+
# if ab_test(:new_page) # true/false test
|
164
|
+
# render action: "new_page"
|
165
|
+
# else
|
166
|
+
# render action: "index"
|
167
|
+
# end
|
168
|
+
# end
|
169
|
+
# @example Similar, alternative value is page name
|
170
|
+
# def index
|
171
|
+
# render action: ab_test(:new_page)
|
172
|
+
# end
|
173
|
+
# @example A/B test inside ERB template (condition)
|
174
|
+
# <%= if ab_test(:banner) %>100% less complexity!<% end %>
|
175
|
+
# @example A/B test inside ERB template (value)
|
176
|
+
# <%= ab_test(:greeting) %> <%= current_user.name %>
|
177
|
+
# @example A/B test inside ERB template (capture)
|
178
|
+
# <% ab_test :features do |count| %>
|
179
|
+
# <%= count %> features to choose from!
|
180
|
+
# <% end %>
|
181
|
+
def ab_test(name, &block)
|
182
|
+
if Vanity.playground.using_js?
|
183
|
+
@_vanity_experiments ||= {}
|
184
|
+
@_vanity_experiments[name] ||= Vanity.playground.experiment(name.to_sym).choose
|
185
|
+
value = @_vanity_experiments[name].value
|
186
|
+
else
|
187
|
+
value = Vanity.playground.experiment(name.to_sym).choose.value
|
188
|
+
end
|
189
|
+
|
190
|
+
if block
|
191
|
+
content = capture(value, &block)
|
192
|
+
block_called_from_erb?(block) ? concat(content) : content
|
193
|
+
else
|
194
|
+
value
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
# Generate url with the identity of the current user and the metric to track on click
|
199
|
+
def vanity_track_url_for(identity, metric, options = {})
|
200
|
+
options = options.merge(:_identity => identity, :_track => metric)
|
201
|
+
url_for(options)
|
202
|
+
end
|
203
|
+
|
204
|
+
# Generate url with the fingerprint for the current Vanity experiment
|
205
|
+
def vanity_tracking_image(identity, metric, options = {})
|
206
|
+
options = options.merge(:controller => :vanity, :action => :image, :_identity => identity, :_track => metric)
|
207
|
+
image_tag(url_for(options), :width => "1px", :height => "1px", :alt => "")
|
208
|
+
end
|
209
|
+
|
210
|
+
def vanity_js
|
211
|
+
return if @_vanity_experiments.nil?
|
212
|
+
javascript_tag do
|
213
|
+
render Vanity.template("vanity.js.erb")
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
def vanity_h(text)
|
218
|
+
h(text)
|
219
|
+
end
|
220
|
+
|
221
|
+
def vanity_html_safe(text)
|
222
|
+
if text.respond_to?(:html_safe)
|
223
|
+
text.html_safe
|
224
|
+
else
|
225
|
+
text
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
def vanity_simple_format(text, html_options={})
|
230
|
+
vanity_html_safe(simple_format(text, html_options))
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
|
235
|
+
# Step 1: Add a new resource in config/routes.rb:
|
236
|
+
# map.vanity "/vanity/:action/:id", :controller=>:vanity
|
237
|
+
#
|
238
|
+
# Step 2: Create a new experiments controller:
|
239
|
+
# class VanityController < ApplicationController
|
240
|
+
# include Vanity::Rails::Dashboard
|
241
|
+
# end
|
242
|
+
#
|
243
|
+
# Step 3: Open your browser to http://localhost:3000/vanity
|
244
|
+
module Dashboard
|
245
|
+
def index
|
246
|
+
render :file=>Vanity.template("_report"), :content_type=>Mime::HTML, :layout=>false
|
247
|
+
end
|
248
|
+
|
249
|
+
def chooses
|
250
|
+
exp = Vanity.playground.experiment(params[:e].to_sym)
|
251
|
+
exp.chooses(exp.alternatives[params[:a].to_i].value)
|
252
|
+
render :file=>Vanity.template("_experiment"), :locals=>{:experiment=>exp}
|
253
|
+
end
|
254
|
+
|
255
|
+
def add_participant
|
256
|
+
if params[:e].nil? || params[:e].empty?
|
257
|
+
render :status => 404, :nothing => true
|
258
|
+
return
|
259
|
+
end
|
260
|
+
exp = Vanity.playground.experiment(params[:e].to_sym)
|
261
|
+
exp.chooses(exp.alternatives[params[:a].to_i].value)
|
262
|
+
render :status => 200, :nothing => true
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
module TrackingImage
|
267
|
+
def image
|
268
|
+
# send image
|
269
|
+
send_file(File.expand_path("../images/x.gif", File.dirname(__FILE__)), :type => 'image/gif', :stream => false, :disposition => 'inline')
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
|
276
|
+
# Enhance ActionController with use_vanity, filters and helper methods.
|
277
|
+
if defined?(ActionController)
|
278
|
+
# Include in controller, add view helper methods.
|
279
|
+
ActionController::Base.class_eval do
|
280
|
+
extend Vanity::Rails::UseVanity
|
281
|
+
include Vanity::Rails::Filters
|
282
|
+
helper Vanity::Rails::Helpers
|
283
|
+
end
|
284
|
+
|
285
|
+
module ActionController
|
286
|
+
class TestCase
|
287
|
+
alias :setup_controller_request_and_response_without_vanity :setup_controller_request_and_response
|
288
|
+
# Sets Vanity.context to the current controller, so you can do things like:
|
289
|
+
# experiment(:simple).chooses(:green)
|
290
|
+
def setup_controller_request_and_response
|
291
|
+
setup_controller_request_and_response_without_vanity
|
292
|
+
Vanity.context = @controller
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
if defined?(ActionMailer)
|
299
|
+
# Include in mailer, add view helper methods.
|
300
|
+
ActionMailer::Base.class_eval do
|
301
|
+
include Vanity::Rails::UseVanityMailer
|
302
|
+
include Vanity::Rails::Filters
|
303
|
+
helper Vanity::Rails::Helpers
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
# Reconnect whenever we fork under Passenger.
|
308
|
+
if defined?(PhusionPassenger)
|
309
|
+
PhusionPassenger.on_event(:starting_worker_process) do |forked|
|
310
|
+
if forked
|
311
|
+
begin
|
312
|
+
Vanity.playground.reconnect! if Vanity.playground.collecting?
|
313
|
+
rescue Exception=>ex
|
314
|
+
Rails.logger.error "Error reconnecting: #{ex.to_s}"
|
315
|
+
end
|
316
|
+
end
|
317
|
+
end
|
318
|
+
end
|