vanity 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -14,65 +14,61 @@ module Vanity
14
14
 
15
15
  end
16
16
 
17
- def initialize(playground, id, name, &block)
17
+ def initialize(playground, id, name, options, &block)
18
18
  @playground = playground
19
19
  @id, @name = id.to_sym, name
20
+ @options = options || {}
20
21
  @namespace = "#{@playground.namespace}:#{@id}"
21
22
  @identify_block = ->(context){ context.vanity_identity }
22
23
  end
23
24
 
24
- # Human readable experiment name, supplied during creation.
25
+ # Human readable experiment name (first argument you pass when creating a
26
+ # new experiment).
25
27
  attr_reader :name
26
28
 
27
- # Unique identifier, derived from name, e.g. "Green Button" become :green_button.
29
+ # Unique identifier, derived from name experiment name, e.g. "Green
30
+ # Button" becomes :green_button.
28
31
  attr_reader :id
29
-
30
- # Experiment creation timestamp.
32
+
33
+ # Time stamp when experiment was created.
31
34
  attr_reader :created_at
32
35
 
33
- # Experiment completion timestamp.
36
+ # Time stamp when experiment was completed.
34
37
  attr_reader :completed_at
35
38
 
36
- # Returns the type of this class as a symbol (e.g. ab_test).
39
+ # Returns the type of this experiment as a symbol (e.g. :ab_test).
37
40
  def type
38
41
  self.class.type
39
42
  end
40
43
 
41
- # Call this method with no argument or block to return an identity. Call
42
- # this method with a block to define how to obtain an identity for the
43
- # current experiment.
44
- #
45
- # For example, this experiment use the identity of the project associated
46
- # with the controller:
47
- #
48
- # class ProjectController < ApplicationController
49
- # before_filter :set_project
50
- # attr_reader :project
44
+ # Defines how we obtain an identity for the current experiment. Usually
45
+ # Vanity gets the identity form a session object (see use_vanity), but
46
+ # there are cases where you want a particular experiment to use a
47
+ # different identity.
51
48
  #
52
- # . . .
53
- # end
54
- #
55
- # experiment "Project widget" do
49
+ # For example, if all your experiments use current_user and you need one
50
+ # experiment to use the current project:
51
+ # ab_test "Project widget" do
56
52
  # alternatives :small, :medium, :large
57
53
  # identify do |controller|
58
54
  # controller.project.id
59
55
  # end
60
56
  # end
61
57
  def identify(&block)
62
- if block_given?
63
- @identify_block = block
64
- self
65
- else
66
- @identify_block.call(Vanity.context) or fail "No identity found"
67
- end
58
+ @identify_block = block
59
+ end
60
+
61
+ def identity
62
+ @identify_block.call(Vanity.context) or fail "No identity found"
68
63
  end
64
+ protected :identity
69
65
 
70
66
 
71
67
  # -- Reporting --
72
68
 
73
69
  # Sets or returns description. For example
74
- # experiment :simple do
75
- # description "Simple experiment"
70
+ # ab_test "Simple" do
71
+ # description "A simple A/B experiment"
76
72
  # end
77
73
  #
78
74
  # puts "Just defined: " + experiment(:simple).description
@@ -90,8 +86,7 @@ module Vanity
90
86
 
91
87
  # Define experiment completion condition. For example:
92
88
  # complete_if do
93
- # alternatives.all? { |alt| alt.participants >= 100 } &&
94
- # alternatives.any? { |alt| alt.confidence >= 0.95 }
89
+ # !score(95).chosen.nil?
95
90
  # end
96
91
  def complete_if(&block)
97
92
  raise ArgumentError, "Missing block" unless block
@@ -129,41 +124,35 @@ module Vanity
129
124
  redis[key(:completed_at)].nil?
130
125
  end
131
126
 
132
-
133
127
  # -- Store/validate --
134
128
 
135
- # Returns key for this experiment, or with an argument, return a key
136
- # using the experiment as the namespace. Examples:
137
- # key => "vanity:experiments:green_button"
138
- # key("participants") => "vanity:experiments:green_button:participants"
139
- def key(name = nil) #:nodoc:
140
- name ? "#{@namespace}:#{name}" : @namespace
129
+ # Get rid of all experiment data.
130
+ def destroy
131
+ redis.del key(:created_at)
132
+ redis.del key(:completed_at)
141
133
  end
142
134
 
143
- # Shortcut for Vanity.playground.redis
144
- def redis #:nodoc:
145
- @playground.redis
146
- end
147
-
148
135
  # Called by Playground to save the experiment definition.
149
136
  def save
150
137
  redis.setnx key(:created_at), Time.now.to_i
151
138
  @created_at = Time.at(redis[key(:created_at)].to_i)
152
139
  end
153
140
 
154
- # Reset experiment to its initial state.
155
- def reset!
156
- @created_at = Time.now
157
- redis[key(:created_at)] = @created_at.to_i
158
- redis.del key(:completed_at)
159
- end
141
+ protected
160
142
 
161
- # Get rid of all experiment data.
162
- def destroy
163
- redis.del key(:created_at)
164
- redis.del key(:completed_at)
143
+ # Returns key for this experiment, or with an argument, return a key
144
+ # using the experiment as the namespace. Examples:
145
+ # key => "vanity:experiments:green_button"
146
+ # key("participants") => "vanity:experiments:green_button:participants"
147
+ def key(name = nil)
148
+ name ? "#{@namespace}:#{name}" : @namespace
165
149
  end
166
150
 
151
+ # Shortcut for Vanity.playground.redis
152
+ def redis
153
+ @playground.redis
154
+ end
155
+
167
156
  end
168
157
  end
169
158
  end
@@ -1,7 +1,21 @@
1
1
  module Vanity
2
2
 
3
- # Vanity.playground.configuration
4
- class Configuration
3
+ # These methods are available from experiment definitions (files located in
4
+ # the experiments directory, automatically loaded by Vanity). Use these
5
+ # methods to define you experiments, for example:
6
+ # ab_test "New Banner" do
7
+ # alternatives :red, :green, :blue
8
+ # end
9
+ module Definition
10
+
11
+ protected
12
+ # Defines a new experiment, given the experiment's name, type and
13
+ # definition block.
14
+ def define(name, type, options = nil, &block)
15
+ options ||= {}
16
+ Vanity.playground.define(name, type, options, &block)
17
+ end
18
+
5
19
  end
6
20
 
7
21
  # Playground catalogs all your experiments, holds the Vanity configuration.
@@ -40,14 +54,12 @@ module Vanity
40
54
  attr_accessor :logger
41
55
 
42
56
  # Defines a new experiment. Generally, do not call this directly,
43
- # use #experiment instead.
44
- def define(name, options = nil, &block)
57
+ # use one of the definition methods (ab_test, measure, etc).
58
+ def define(name, type, options = {}, &block)
45
59
  id = name.to_s.downcase.gsub(/\W/, "_")
46
60
  raise "Experiment #{id} already defined once" if @experiments[id]
47
- options ||= {}
48
- type = options[:type] || :ab_test
49
61
  klass = Experiment.const_get(type.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase })
50
- experiment = klass.new(self, id, name)
62
+ experiment = klass.new(self, id, name, options)
51
63
  experiment.instance_eval &block
52
64
  experiment.save
53
65
  @experiments[id] = experiment
@@ -62,7 +74,23 @@ module Vanity
62
74
  def experiment(name)
63
75
  id = name.to_s.downcase.gsub(/\W/, "_")
64
76
  unless @experiments.has_key?(id)
65
- require File.join(load_path, id)
77
+ @loading ||= []
78
+ fail "Circular dependency detected: #{@loading.join('=>')}=>#{id}" if @loading.include?(id)
79
+ begin
80
+ @loading.push id
81
+ source = File.read(File.expand_path("#{id}.rb", load_path))
82
+ context = Object.new
83
+ context.instance_eval do
84
+ extend Definition
85
+ eval source
86
+ end
87
+ rescue
88
+ error = LoadError.exception($!.message)
89
+ error.set_backtrace $!.backtrace
90
+ raise error
91
+ ensure
92
+ @loading.pop
93
+ end
66
94
  end
67
95
  @experiments[id] or fail LoadError, "Expected experiments/#{id}.rb to define experiment #{name}"
68
96
  end
@@ -70,11 +98,17 @@ module Vanity
70
98
  # Returns list of all loaded experiments.
71
99
  def experiments
72
100
  Dir[File.join(load_path, "*.rb")].each do |file|
73
- require file
101
+ id = File.basename(file).gsub(/.rb$/, "")
102
+ experiment id
74
103
  end
75
104
  @experiments.values
76
105
  end
77
106
 
107
+ # Reloads all experiments.
108
+ def reload!
109
+ @experiments.clear
110
+ end
111
+
78
112
  # Use this instance to access the Redis database.
79
113
  def redis
80
114
  redis = Redis.new(host: self.host, port: self.port, db: self.db,
@@ -100,7 +134,7 @@ module Vanity
100
134
  end
101
135
 
102
136
  # Sets the Vanity context. For example, when using Rails this would be
103
- # set by the set_vanity_context before filter (via use_vanity).
137
+ # set by the set_vanity_context before filter (via Vanity::Rails#use_vanity).
104
138
  def context=(context)
105
139
  Thread.current[:vanity_context] = context
106
140
  end
@@ -115,22 +149,12 @@ module Vanity
115
149
  end
116
150
  end
117
151
 
152
+
118
153
  class Object
119
154
 
120
- # Use this method to define or access an experiment.
121
- #
122
- # To define an experiment, call with a name, options and a block. For
123
- # example:
124
- # experiment "Text size" do
125
- # alternatives :small, :medium, :large
126
- # end
127
- #
155
+ # Use this method to access an experiment by name. For example:
128
156
  # puts experiment(:text_size).alternatives
129
- def experiment(name, options = nil, &block)
130
- if block
131
- Vanity.playground.define(name, options, &block)
132
- else
133
- Vanity.playground.experiment(name)
134
- end
157
+ def experiment(name)
158
+ Vanity.playground.experiment(name)
135
159
  end
136
160
  end
data/lib/vanity/rails.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  require "vanity"
2
2
  require "vanity/rails/helpers"
3
3
  require "vanity/rails/testing"
4
- require "vanity/rails/console"
4
+ require "vanity/rails/dashboard"
5
5
 
6
6
  # Use Rails logger by default.
7
7
  Vanity.playground.logger ||= ActionController::Base.logger
@@ -9,6 +9,7 @@ Vanity.playground.load_path = "#{RAILS_ROOT}/experiments"
9
9
 
10
10
  # Include in controller, add view helper methods.
11
11
  ActionController::Base.class_eval do
12
+ extend Vanity::Rails::ClassMethods
12
13
  include Vanity::Rails
13
14
  helper Vanity::Rails
14
15
  end
@@ -0,0 +1,15 @@
1
+ module Vanity
2
+ module Rails
3
+ module Dashboard
4
+ def index
5
+ render Vanity.template("_report"), content_type: Mime::HTML, layout: true
6
+ end
7
+
8
+ def chooses
9
+ exp = Vanity.playground.experiment(params[:e])
10
+ exp.chooses(exp.alternatives[params[:a].to_i].value)
11
+ render partial: Vanity.template("experiment"), locals: { experiment: exp }
12
+ end
13
+ end
14
+ end
15
+ end
@@ -14,43 +14,33 @@ module Vanity
14
14
  # 3) Measure conversion:
15
15
  #
16
16
  # def signup
17
- # ab_goal! :pricing
17
+ # track! :pricing
18
18
  # . . .
19
19
  # end
20
20
  module Rails
21
21
  module ClassMethods
22
22
 
23
- # Defines the vanity_identity method, and the set_identity_context before
24
- # filter.
23
+ # Defines the vanity_identity method and the set_identity_context filter.
25
24
  #
26
- # Single argument names a method that returns an object whose identity is
27
- # the vanity identity. Identity is used to present an experiment
28
- # consistently to the same person or people. It can be the user's
29
- # identity, group, project. The object must provide its identity in
30
- # response to the method +id+.
31
- #
32
- # For example, if +current_user+ returns a +User+ object, then to use the
33
- # user's id:
25
+ # Call with the name of a method that returns an object whose identity
26
+ # will be used as the Vanity identity. Confusing? Let's try by example:
27
+ #
34
28
  # class ApplicationController < ActionController::Base
35
29
  # use_vanity :current_user
36
- # end
37
- #
38
- # If that method returns nil (e.g. the user has not signed in), a random
39
- # value will be used, instead. That random value is maintained using a
40
- # cookie.
41
- #
42
- # Alternatively, if you call use_vanity with a block, it will yield to the
43
- # block with controller.
44
- #
45
- # If there is no identity you can use, call use_vanity with the value +nil+.
46
30
  #
47
- # For example:
48
- # class ApplicationController < ActionController::Base
49
- # use_vanity :current_user
31
+ # def current_user
32
+ # User.find(session[:user_id])
33
+ # end
50
34
  # end
35
+ #
36
+ # If that method (current_user in this example) returns nil, Vanity will
37
+ # set the identity for you (using a cookie to remember it across
38
+ # requests). It also uses this mechanism if you don't provide an
39
+ # identity object, by calling use_vanity with no arguments.
51
40
  #
41
+ # Of course you can also use a block:
52
42
  # class ProjectController < ApplicationController
53
- # use_vanity { |controller| controller.project_id }
43
+ # use_vanity { |controller| controller.params[:project_id] }
54
44
  # end
55
45
  def use_vanity(symbol = nil, &block)
56
46
  define_method :vanity_identity do
@@ -71,13 +61,10 @@ module Vanity
71
61
  Vanity.context = self
72
62
  end
73
63
  before_filter :set_vanity_context
64
+ before_filter { Vanity.playground.reload! } unless ::Rails.configuration.cache_classes
74
65
  end
75
66
  end
76
67
 
77
- def self.included(base) #:nodoc:
78
- base.extend ClassMethods
79
- end
80
-
81
68
  # This method returns one of the alternative values in the named A/B test.
82
69
  #
83
70
  # Examples using ab_test inside controller:
@@ -113,11 +100,11 @@ module Vanity
113
100
 
114
101
  # This method records conversion on the named A/B test. For example:
115
102
  # def create
116
- # ab_goal! :call_to_action
103
+ # track! :call_to_action
117
104
  # Acccount.create! params[:account]
118
105
  # end
119
- def ab_goal!(name)
120
- Vanity.playground.experiment(name).conversion!
106
+ def track!(name, *args)
107
+ Vanity.playground.experiment(name).track! *args
121
108
  end
122
109
  end
123
110
  end
@@ -1,6 +1,8 @@
1
- module ActionController #:nodoc:
1
+ module ActionController
2
2
  class TestCase
3
3
  alias :setup_controller_request_and_response_without_vanity :setup_controller_request_and_response
4
+ # Sets Vanity.context to the current controller, so you can do things like:
5
+ # experiment(:simple).chooses(:green)
4
6
  def setup_controller_request_and_response
5
7
  setup_controller_request_and_response_without_vanity
6
8
  Vanity.context = @controller
@@ -1,21 +1,23 @@
1
1
  <% score = experiment.score %>
2
2
  <table>
3
- <caption><%= experiment.conclusion(score).join(" ") %></caption>
3
+ <caption>
4
+ <%= experiment.conclusion(score).join(" ") %></caption>
4
5
  <% score.alts.each do |alt| %>
5
6
  <tr class="<%= "choice" if score.choice == alt %>">
6
7
  <td class="option"><%= alt.name.gsub(/^o/, "O") %>:</td>
7
- <td class="value"><code><%= CGI.escape_html alt.value.to_s %></code></td>
8
+ <td class="value"><code><%=h alt.value.to_s %></code></td>
8
9
  <td>
9
10
  <%= "%.1f%%" % [alt.conversion_rate * 100] %>
10
11
  <%= "(%d%% better than %s)" % [alt.difference, score.least.name] if alt.difference && alt.difference >= 1 %>
11
12
  </td>
12
13
  <td class="action">
13
14
  <% if experiment.active? && respond_to?(:chooses_experiments_url) %>
14
- <% if experiment.chosen?(alt) %>
15
+ <% if experiment.showing?(alt) %>
15
16
  showing
16
17
  <% else %>
17
- <%= link_to "show", chooses_experiments_url(e: experiment.id, a: alt.id), method: :post,
18
- class: "button", title: "Show me this alternative from now on" %>
18
+ <%= link_to_remote "show", update: "experiment_#{experiment.id}",
19
+ url: chooses_experiments_url(e: experiment.id, a: alt.id), method: :post,
20
+ html: { class: "button", title: "Show me this alternative from now on" } %>
19
21
  <% end %>
20
22
  <% end %>
21
23
  </td>
@@ -0,0 +1,5 @@
1
+ <h2><%=h experiment.name %> <span class="type">(<%= experiment.class.friendly_name %>)</span></h2>
2
+ <%= experiment.description.to_s.split(/\n\s*\n/).map { |para| %{<p class="description">#{h para}</p>} }.join %>
3
+ <%= render Vanity.template(experiment.type), experiment: experiment %>
4
+ <p class="meta">Started <%= experiment.created_at.strftime("%a, %b %-d") %>
5
+ <%= " | Completed #{experiment.completed_at.strftime("%a, %b %-d")}" unless experiment.active? %></p>