moses-vanity 1.7.1
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/.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
|