vanity 2.0.1 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +1 -2
- data/Appraisals +6 -6
- data/CHANGELOG +9 -3
- data/Gemfile.lock +1 -1
- data/README.md +299 -0
- data/doc/configuring.textile +8 -1
- data/doc/identity.textile +2 -0
- data/doc/metrics.textile +10 -0
- data/gemfiles/rails32.gemfile.lock +1 -1
- data/gemfiles/rails41.gemfile.lock +1 -1
- data/gemfiles/rails42.gemfile.lock +1 -1
- data/gemfiles/{rails4.gemfile → rails42_protected_attributes.gemfile} +2 -2
- data/gemfiles/rails42_protected_attributes.gemfile.lock +209 -0
- data/lib/generators/templates/vanity_migration.rb +1 -0
- data/lib/vanity/adapters/abstract_adapter.rb +11 -0
- data/lib/vanity/adapters/active_record_adapter.rb +15 -1
- data/lib/vanity/adapters/mock_adapter.rb +14 -0
- data/lib/vanity/adapters/mongodb_adapter.rb +14 -0
- data/lib/vanity/adapters/redis_adapter.rb +15 -0
- data/lib/vanity/configuration.rb +43 -11
- data/lib/vanity/experiment/ab_test.rb +145 -15
- data/lib/vanity/experiment/alternative.rb +4 -0
- data/lib/vanity/frameworks/rails.rb +69 -31
- data/lib/vanity/locales/vanity.en.yml +9 -0
- data/lib/vanity/locales/vanity.pt-BR.yml +4 -0
- data/lib/vanity/metric/active_record.rb +9 -1
- data/lib/vanity/templates/_ab_test.erb +9 -2
- data/lib/vanity/templates/_experiment.erb +21 -1
- data/lib/vanity/templates/vanity.css +11 -3
- data/lib/vanity/templates/vanity.js +35 -6
- data/lib/vanity/version.rb +1 -1
- data/test/commands/report_test.rb +1 -0
- data/test/dummy/config/application.rb +1 -0
- data/test/experiment/ab_test.rb +414 -0
- data/test/experiment/base_test.rb +16 -10
- data/test/frameworks/rails/action_controller_test.rb +14 -6
- data/test/frameworks/rails/action_mailer_test.rb +8 -6
- data/test/frameworks/rails/action_view_test.rb +1 -0
- data/test/helper_test.rb +2 -0
- data/test/metric/active_record_test.rb +56 -0
- data/test/playground_test.rb +3 -0
- data/test/test_helper.rb +28 -2
- data/test/web/rails/dashboard_test.rb +2 -0
- data/vanity.gemspec +2 -2
- metadata +8 -8
- data/README.rdoc +0 -231
- data/gemfiles/rails4.gemfile.lock +0 -179
@@ -67,6 +67,17 @@ module Vanity
|
|
67
67
|
fail "Not implemented"
|
68
68
|
end
|
69
69
|
|
70
|
+
# Store whether an experiment is enabled or not
|
71
|
+
def set_experiment_enabled(experiment, enabled)
|
72
|
+
fail "Not implemented"
|
73
|
+
end
|
74
|
+
|
75
|
+
# Returns true if experiment is enabled, the default (Vanity.configuration.experiments_start_enabled) is true.
|
76
|
+
# (*except for mock_adapter, where default is true for testing)
|
77
|
+
def is_experiment_enabled?(experiment)
|
78
|
+
fail "Not implemented"
|
79
|
+
end
|
80
|
+
|
70
81
|
# Returns counts for given A/B experiment and alternative (by index).
|
71
82
|
# Returns hash with values for the keys :participants, :converted and
|
72
83
|
# :conversions.
|
@@ -71,7 +71,7 @@ module Vanity
|
|
71
71
|
|
72
72
|
def increment_conversion(alternative, count = 1)
|
73
73
|
record = vanity_conversions.rails_agnostic_find_or_create_by(:alternative, alternative)
|
74
|
-
record.
|
74
|
+
record.class.update_counters(record.id, conversions: count)
|
75
75
|
end
|
76
76
|
end
|
77
77
|
|
@@ -79,6 +79,7 @@ module Vanity
|
|
79
79
|
class VanityConversion < VanityRecord
|
80
80
|
self.table_name = :vanity_conversions
|
81
81
|
belongs_to :vanity_experiment
|
82
|
+
attr_accessible :alternative if needs_attr_accessible?
|
82
83
|
end
|
83
84
|
|
84
85
|
# Participant model
|
@@ -201,6 +202,19 @@ module Vanity
|
|
201
202
|
!!VanityExperiment.retrieve(experiment).completed_at
|
202
203
|
end
|
203
204
|
|
205
|
+
def set_experiment_enabled(experiment, enabled)
|
206
|
+
VanityExperiment.retrieve(experiment).update_attribute(:enabled, enabled)
|
207
|
+
end
|
208
|
+
|
209
|
+
def is_experiment_enabled?(experiment)
|
210
|
+
record = VanityExperiment.retrieve(experiment)
|
211
|
+
if Vanity.configuration.experiments_start_enabled
|
212
|
+
record.enabled != false
|
213
|
+
else
|
214
|
+
record.enabled == true
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
204
218
|
# Returns counts for given A/B experiment and alternative (by index).
|
205
219
|
# Returns hash with values for the keys :participants, :converted and
|
206
220
|
# :conversions.
|
@@ -95,6 +95,20 @@ module Vanity
|
|
95
95
|
def is_experiment_completed?(experiment)
|
96
96
|
@experiments[experiment] && @experiments[experiment][:completed_at]
|
97
97
|
end
|
98
|
+
|
99
|
+
def set_experiment_enabled(experiment, enabled)
|
100
|
+
@experiments[experiment] ||= {}
|
101
|
+
@experiments[experiment][:enabled] = enabled
|
102
|
+
end
|
103
|
+
|
104
|
+
def is_experiment_enabled?(experiment)
|
105
|
+
record = @experiments[experiment]
|
106
|
+
if Vanity.configuration.experiments_start_enabled
|
107
|
+
record == nil || record[:enabled] != false
|
108
|
+
else
|
109
|
+
record && record[:enabled] == true
|
110
|
+
end
|
111
|
+
end
|
98
112
|
|
99
113
|
def ab_counts(experiment, alternative)
|
100
114
|
@experiments[experiment] ||= {}
|
@@ -114,6 +114,7 @@ module Vanity
|
|
114
114
|
def get_experiment_created_at(experiment)
|
115
115
|
record = @experiments.find_one({ :_id=>experiment }, { :fields=>[:created_at] })
|
116
116
|
record && record["created_at"]
|
117
|
+
#Returns nil if either the record or the field doesn't exist
|
117
118
|
end
|
118
119
|
|
119
120
|
def set_experiment_completed_at(experiment, time)
|
@@ -129,6 +130,19 @@ module Vanity
|
|
129
130
|
!!@experiments.find_one(:_id=>experiment, :completed_at=>{ "$exists"=>true })
|
130
131
|
end
|
131
132
|
|
133
|
+
def set_experiment_enabled(experiment, enabled)
|
134
|
+
@experiments.update({ :_id=>experiment }, { "$set"=>{ :enabled=>enabled } }, :upsert=>true)
|
135
|
+
end
|
136
|
+
|
137
|
+
def is_experiment_enabled?(experiment)
|
138
|
+
record = @experiments.find_one({ :_id=>experiment}, { :fields=>[:enabled] })
|
139
|
+
if Vanity.configuration.experiments_start_enabled
|
140
|
+
record == nil || record["enabled"] != false
|
141
|
+
else
|
142
|
+
record && record["enabled"] == true
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
132
146
|
def ab_counts(experiment, alternative)
|
133
147
|
record = @experiments.find_one({ :_id=>experiment }, { :fields=>[:conversions] })
|
134
148
|
conversions = record && record["conversions"]
|
@@ -127,6 +127,21 @@ module Vanity
|
|
127
127
|
end
|
128
128
|
end
|
129
129
|
|
130
|
+
def set_experiment_enabled(experiment, enabled)
|
131
|
+
call_redis_with_failover do
|
132
|
+
@experiments.set "#{experiment}:enabled", enabled
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def is_experiment_enabled?(experiment)
|
137
|
+
value = @experiments["#{experiment}:enabled"]
|
138
|
+
if Vanity.configuration.experiments_start_enabled
|
139
|
+
value != 'false'
|
140
|
+
else
|
141
|
+
value == 'true'
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
130
145
|
def ab_counts(experiment, alternative)
|
131
146
|
{
|
132
147
|
:participants => @experiments.scard("#{experiment}:alts:#{alternative}:participants").to_i,
|
data/lib/vanity/configuration.rb
CHANGED
@@ -25,30 +25,40 @@ module Vanity
|
|
25
25
|
nil
|
26
26
|
end
|
27
27
|
|
28
|
+
#
|
29
|
+
# Filter all User-Agents that have 'bot', 'crawler', 'spider', URL.
|
30
|
+
#
|
28
31
|
def default_request_filter(request) # :nodoc:
|
29
32
|
request &&
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
+
request.env &&
|
34
|
+
request.env["HTTP_USER_AGENT"] &&
|
35
|
+
request.env["HTTP_USER_AGENT"].match( /(?:https?:\/\/)|(?:bot|spider|crawler)/i )
|
33
36
|
end
|
34
37
|
end
|
35
38
|
|
36
39
|
DEFAULTS = {
|
40
|
+
add_participant_route: "/vanity/add_participant",
|
37
41
|
collecting: true,
|
42
|
+
config_file: "vanity.yml",
|
43
|
+
config_path: File.join(Pathname.new("."), "config"),
|
44
|
+
environment: ENV["RACK_ENV"] || ENV["RAILS_ENV"] || "development",
|
38
45
|
experiments_path: File.join(Pathname.new("."), "experiments"),
|
39
|
-
add_participant_route: "/vanity/add_participant",
|
40
|
-
logger: default_logger,
|
41
46
|
failover_on_datastore_error: false,
|
47
|
+
locales_path: File.expand_path(File.join(File.dirname(__FILE__), 'locales')),
|
48
|
+
logger: default_logger,
|
42
49
|
on_datastore_error: ->(error, klass, method, arguments) {
|
43
50
|
default_on_datastore_error(error, klass, method, arguments)
|
44
51
|
},
|
45
52
|
request_filter: ->(request) { default_request_filter(request) },
|
46
53
|
templates_path: File.expand_path(File.join(File.dirname(__FILE__), 'templates')),
|
47
|
-
locales_path: File.expand_path(File.join(File.dirname(__FILE__), 'locales')),
|
48
54
|
use_js: false,
|
49
|
-
|
50
|
-
|
51
|
-
|
55
|
+
experiments_start_enabled: true,
|
56
|
+
cookie_name: 'vanity_id',
|
57
|
+
cookie_expires: 20 * 365 * 24 * 60 * 60, # 20 years, give or take.
|
58
|
+
cookie_domain: nil,
|
59
|
+
cookie_path: nil,
|
60
|
+
cookie_secure: false,
|
61
|
+
cookie_httponly: false,
|
52
62
|
}.freeze
|
53
63
|
|
54
64
|
# True if saving results to the datastore (participants and conversions).
|
@@ -101,8 +111,9 @@ module Vanity
|
|
101
111
|
# end
|
102
112
|
#
|
103
113
|
# The default implementation does a simple test of whether the request's
|
104
|
-
# HTTP_USER_AGENT header contains a URI,
|
105
|
-
#
|
114
|
+
# HTTP_USER_AGENT header contains a URI, or the words 'bot', 'crawler', or
|
115
|
+
# 'spider' since well behaved bots typically include a reference URI in
|
116
|
+
# their user agent strings. (Original idea:
|
106
117
|
# http://stackoverflow.com/a/9285889.)
|
107
118
|
#
|
108
119
|
# Alternatively, one could filter an explicit list of IPs, add additional
|
@@ -147,7 +158,28 @@ module Vanity
|
|
147
158
|
attr_writer :config_file
|
148
159
|
# In order of precedence, RACK_ENV, RAILS_ENV or `development`.
|
149
160
|
attr_writer :environment
|
161
|
+
# By default experiments start enabled. If you want experiments to be
|
162
|
+
# explicitly enabled after a production release, then set to false.
|
163
|
+
attr_writer :experiments_start_enabled
|
164
|
+
|
165
|
+
# Cookie name. By default 'vanity_id'
|
166
|
+
attr_writer :cookie_name
|
167
|
+
|
168
|
+
# Cookie duration. By default 20 years.
|
169
|
+
attr_writer :cookie_expires
|
170
|
+
|
171
|
+
# Cookie domain. By default nil. If domain is nil then the domain from
|
172
|
+
# Rails.application.config.session_options[:domain] will be substituted.
|
173
|
+
attr_writer :cookie_domain
|
174
|
+
|
175
|
+
# Cookie path. By default nil.
|
176
|
+
attr_writer :cookie_path
|
177
|
+
|
178
|
+
# Cookie secure. If true, cookie will only be transmitted to SSL pages. By default false.
|
179
|
+
attr_writer :cookie_secure
|
150
180
|
|
181
|
+
# Cookie path. If true, cookie will not be available to JS. By default false.
|
182
|
+
attr_writer :cookie_httponly
|
151
183
|
|
152
184
|
# We independently list each attr_accessor to includes docs, otherwise
|
153
185
|
# something like DEFAULTS.each { |key, value| attr_accessor key } would be
|
@@ -25,6 +25,56 @@ module Vanity
|
|
25
25
|
super
|
26
26
|
@score_method = DEFAULT_SCORE_METHOD
|
27
27
|
@use_probabilities = nil
|
28
|
+
@is_default_set = false
|
29
|
+
end
|
30
|
+
|
31
|
+
# -- Default --
|
32
|
+
|
33
|
+
# Call this method once to set a default alternative. Call without
|
34
|
+
# arguments to obtain the current default. If default is not specified,
|
35
|
+
# the first alternative is used.
|
36
|
+
#
|
37
|
+
# @example Set the default alternative
|
38
|
+
# ab_test "Background color" do
|
39
|
+
# alternatives "red", "blue", "orange"
|
40
|
+
# default "red"
|
41
|
+
# end
|
42
|
+
# @example Get the default alternative
|
43
|
+
# assert experiment(:background_color).default == "red"
|
44
|
+
#
|
45
|
+
def default(value)
|
46
|
+
@default = value
|
47
|
+
@is_default_set = true
|
48
|
+
class << self
|
49
|
+
define_method :default do |*args|
|
50
|
+
raise ArgumentError, "default has already been set to #{@default.inspect}" unless args.empty?
|
51
|
+
alternative(@default)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
nil
|
55
|
+
end
|
56
|
+
|
57
|
+
# -- Enabled --
|
58
|
+
|
59
|
+
# Returns true if experiment is enabled, false if disabled.
|
60
|
+
def enabled?
|
61
|
+
!@playground.collecting? || ( active? && connection.is_experiment_enabled?(@id) )
|
62
|
+
end
|
63
|
+
|
64
|
+
# Enable or disable the experiment. Only works if the playground is collecting
|
65
|
+
# and this experiment is enabled.
|
66
|
+
#
|
67
|
+
# **Note** You should *not* set the enabled/disabled status of an
|
68
|
+
# experiment until it exists in the database. Ensure that your experiment
|
69
|
+
# has had #save invoked previous to any enabled= calls.
|
70
|
+
def enabled=(bool)
|
71
|
+
return unless @playground.collecting? && active?
|
72
|
+
if created_at.nil?
|
73
|
+
warn 'DB has no created_at for this experiment! This most likely means' +
|
74
|
+
'you didn\'t call #save before calling enabled=, which you should.'
|
75
|
+
else
|
76
|
+
connection.set_experiment_enabled(@id, bool)
|
77
|
+
end
|
28
78
|
end
|
29
79
|
|
30
80
|
# -- Metric --
|
@@ -47,7 +97,9 @@ module Vanity
|
|
47
97
|
|
48
98
|
# Call this method once to set alternative values for this experiment
|
49
99
|
# (requires at least two values). Call without arguments to obtain
|
50
|
-
# current list of alternatives.
|
100
|
+
# current list of alternatives. Call with a hash to set custom
|
101
|
+
# probabilities. If providing a hash of alternates, you may need to
|
102
|
+
# specify a default unless your hashes are ordered. (Ruby >= 1.9)
|
51
103
|
#
|
52
104
|
# @example Define A/B test with three alternatives
|
53
105
|
# ab_test "Background color" do
|
@@ -55,13 +107,21 @@ module Vanity
|
|
55
107
|
# alternatives "red", "blue", "orange"
|
56
108
|
# end
|
57
109
|
#
|
110
|
+
# @example Define A/B test with custom probabilities
|
111
|
+
# ab_test "Background color" do
|
112
|
+
# metrics :coolness
|
113
|
+
# alternatives "red" => 10, "blue" => 5, "orange => 1
|
114
|
+
# default "red"
|
115
|
+
# end
|
116
|
+
#
|
58
117
|
# @example Find out which alternatives this test uses
|
59
118
|
# alts = experiment(:background_color).alternatives
|
60
119
|
# puts "#{alts.count} alternatives, with the colors: #{alts.map(&:value).join(", ")}"
|
61
120
|
def alternatives(*args)
|
62
|
-
|
63
|
-
|
64
|
-
|
121
|
+
if has_alternative_weights?(args)
|
122
|
+
build_alternatives_with_weights(args)
|
123
|
+
else
|
124
|
+
build_alternatives(args)
|
65
125
|
end
|
66
126
|
end
|
67
127
|
|
@@ -126,16 +186,18 @@ module Vanity
|
|
126
186
|
def choose(request=nil)
|
127
187
|
if @playground.collecting?
|
128
188
|
if active?
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
189
|
+
if enabled?
|
190
|
+
index = alternative_index_for_identity(request)
|
191
|
+
else
|
192
|
+
# Show the default if experiment is disabled.
|
193
|
+
index = alternatives.index(default)
|
134
194
|
end
|
135
195
|
else
|
196
|
+
# If inactive, always show the outcome. Fallback to generation if one can't be found.
|
136
197
|
index = connection.ab_get_outcome(@id) || alternative_for(identity)
|
137
198
|
end
|
138
199
|
else
|
200
|
+
# If collecting=false, show the alternative, but don't track anything.
|
139
201
|
identity = identity()
|
140
202
|
@showing ||= {}
|
141
203
|
@showing[identity] ||= alternative_for(identity)
|
@@ -145,7 +207,6 @@ module Vanity
|
|
145
207
|
alternatives[index.to_i]
|
146
208
|
end
|
147
209
|
|
148
|
-
|
149
210
|
# -- Testing and JS Callback --
|
150
211
|
|
151
212
|
# Forces this experiment to use a particular alternative. This may be
|
@@ -409,7 +470,9 @@ module Vanity
|
|
409
470
|
end
|
410
471
|
|
411
472
|
def complete!(outcome = nil)
|
473
|
+
# This statement is equivalent to: return unless collecting?
|
412
474
|
return unless @playground.collecting? && active?
|
475
|
+
self.enabled = false
|
413
476
|
super
|
414
477
|
|
415
478
|
unless outcome
|
@@ -426,28 +489,56 @@ module Vanity
|
|
426
489
|
end
|
427
490
|
end
|
428
491
|
# TODO: logging
|
429
|
-
connection.ab_set_outcome
|
492
|
+
connection.ab_set_outcome(@id, outcome || 0)
|
430
493
|
end
|
431
494
|
|
432
495
|
|
433
496
|
# -- Store/validate --
|
434
497
|
|
435
498
|
def destroy
|
436
|
-
connection.destroy_experiment
|
499
|
+
connection.destroy_experiment(@id)
|
437
500
|
super
|
438
501
|
end
|
502
|
+
|
503
|
+
# clears all collected data for the experiment
|
504
|
+
def reset
|
505
|
+
return unless @playground.collecting?
|
506
|
+
connection.destroy_experiment(@id)
|
507
|
+
connection.set_experiment_created_at(@id, Time.now)
|
508
|
+
@outcome = @completed_at = nil
|
509
|
+
self
|
510
|
+
end
|
439
511
|
|
440
512
|
# clears all collected data for the experiment
|
441
513
|
def reset
|
442
|
-
connection.destroy_experiment
|
443
|
-
connection.set_experiment_created_at
|
514
|
+
connection.destroy_experiment(@id)
|
515
|
+
connection.set_experiment_created_at(@id, Time.now)
|
444
516
|
@outcome = @completed_at = nil
|
445
517
|
self
|
446
518
|
end
|
447
519
|
|
520
|
+
# Set up tracking for metrics and ensure that the attributes of the ab_test
|
521
|
+
# are valid (e.g. has alternatives, has a default, has metrics).
|
522
|
+
# If collecting, this method will also store this experiment into the db.
|
523
|
+
# In most cases, you call this method right after the experiment's been instantiated
|
524
|
+
# and declared.
|
448
525
|
def save
|
526
|
+
if @saved
|
527
|
+
warn "Experiment #{name} has already been saved"
|
528
|
+
return
|
529
|
+
end
|
530
|
+
@saved = true
|
449
531
|
true_false unless @alternatives
|
450
532
|
fail "Experiment #{name} needs at least two alternatives" unless @alternatives.size >= 2
|
533
|
+
if !@is_default_set
|
534
|
+
default(@alternatives.first)
|
535
|
+
warn "No default alternative specified; choosing #{@default} as default."
|
536
|
+
elsif alternative(@default).nil?
|
537
|
+
#Specified a default that wasn't listed as an alternative; warn and override.
|
538
|
+
warn "Attempted to set unknown alternative #{@default} as default! Using #{@alternatives.first} instead."
|
539
|
+
#Set the instance variable directly since default(value) is no longer defined
|
540
|
+
@default = @alternatives.first
|
541
|
+
end
|
451
542
|
super
|
452
543
|
if @metrics.nil? || @metrics.empty?
|
453
544
|
warn "Please use metrics method to explicitly state which metric you are measuring against."
|
@@ -461,7 +552,7 @@ module Vanity
|
|
461
552
|
|
462
553
|
# Called via a hook by the associated metric.
|
463
554
|
def track!(metric_id, timestamp, count, *args)
|
464
|
-
return unless active?
|
555
|
+
return unless active? && enabled?
|
465
556
|
identity = args.last[:identity] if args.last.is_a?(Hash)
|
466
557
|
identity ||= identity() rescue nil
|
467
558
|
if identity
|
@@ -495,6 +586,18 @@ module Vanity
|
|
495
586
|
end
|
496
587
|
end
|
497
588
|
|
589
|
+
# Returns the assigned alternative, previously chosen alternative, or
|
590
|
+
# alternative_for for a given identity.
|
591
|
+
def alternative_index_for_identity(request)
|
592
|
+
identity = identity()
|
593
|
+
index = connection.ab_showing(@id, identity) || connection.ab_assigned(@id, identity)
|
594
|
+
unless index
|
595
|
+
index = alternative_for(identity).to_i
|
596
|
+
save_assignment_if_valid_visitor(identity, index, request) unless @playground.using_js?
|
597
|
+
end
|
598
|
+
index
|
599
|
+
end
|
600
|
+
|
498
601
|
# Chooses an alternative for the identity and returns its index. This
|
499
602
|
# method always returns the same alternative for a given experiment and
|
500
603
|
# identity, and randomly distributed alternatives for each identity (in the
|
@@ -550,6 +653,33 @@ module Vanity
|
|
550
653
|
end
|
551
654
|
end
|
552
655
|
|
656
|
+
def has_alternative_weights?(args)
|
657
|
+
@alternatives.nil? && args.size == 1 && args[0].is_a?(Hash)
|
658
|
+
end
|
659
|
+
|
660
|
+
def build_alternatives_with_weights(args)
|
661
|
+
@alternatives = args[0]
|
662
|
+
sum_of_probability = @alternatives.values.reduce(0) { |a,b| a+b }
|
663
|
+
cumulative_probability = 0.0
|
664
|
+
@use_probabilities = []
|
665
|
+
result = []
|
666
|
+
@alternatives = @alternatives.each_with_index.map do |(value, probability), i|
|
667
|
+
result << alternative = Alternative.new( self, i, value )
|
668
|
+
probability = probability.to_f / sum_of_probability
|
669
|
+
@use_probabilities << [ alternative, cumulative_probability += probability ]
|
670
|
+
value
|
671
|
+
end
|
672
|
+
|
673
|
+
result
|
674
|
+
end
|
675
|
+
|
676
|
+
def build_alternatives(args)
|
677
|
+
@alternatives ||= args.empty? ? [true, false] : args.clone
|
678
|
+
@alternatives.each_with_index.map do |value, i|
|
679
|
+
Alternative.new(self, i, value)
|
680
|
+
end
|
681
|
+
end
|
682
|
+
|
553
683
|
begin
|
554
684
|
a = 50.0
|
555
685
|
# Returns array of [z-score, percentage]
|
@@ -53,6 +53,19 @@ module Vanity
|
|
53
53
|
define_method :vanity_identity do
|
54
54
|
return @vanity_identity if @vanity_identity
|
55
55
|
|
56
|
+
cookie = lambda do |value|
|
57
|
+
result = {
|
58
|
+
value: value,
|
59
|
+
expires: Time.now + Vanity.configuration.cookie_expires,
|
60
|
+
path: Vanity.configuration.cookie_path,
|
61
|
+
domain: Vanity.configuration.cookie_domain,
|
62
|
+
secure: Vanity.configuration.cookie_secure,
|
63
|
+
httponly: Vanity.configuration.cookie_httponly
|
64
|
+
}
|
65
|
+
result[:domain] ||= ::Rails.application.config.session_options[:domain]
|
66
|
+
result
|
67
|
+
end
|
68
|
+
|
56
69
|
# With user sign in, it's possible to visit not-logged in, get
|
57
70
|
# cookied and shown alternative A, then sign in and based on
|
58
71
|
# user.id, get shown alternative B.
|
@@ -60,18 +73,15 @@ module Vanity
|
|
60
73
|
# new user.id to avoid the flash of alternative B (FOAB).
|
61
74
|
if request.get? && params[:_identity]
|
62
75
|
@vanity_identity = params[:_identity]
|
63
|
-
cookies[
|
76
|
+
cookies[Vanity.configuration.cookie_name] = cookie.call(@vanity_identity)
|
64
77
|
@vanity_identity
|
65
|
-
elsif cookies[
|
66
|
-
@vanity_identity = cookies[
|
78
|
+
elsif cookies[Vanity.configuration.cookie_name]
|
79
|
+
@vanity_identity = cookies[Vanity.configuration.cookie_name]
|
67
80
|
elsif symbol && object = send(symbol)
|
68
81
|
@vanity_identity = object.id
|
69
82
|
elsif response # everyday use
|
70
|
-
@vanity_identity = cookies[
|
71
|
-
|
72
|
-
# Useful if application and admin console are on separate domains.
|
73
|
-
cookie[:domain] ||= ::Rails.application.config.session_options[:domain]
|
74
|
-
cookies["vanity_id"] = cookie
|
83
|
+
@vanity_identity = cookies[Vanity.configuration.cookie_name] || SecureRandom.hex(16)
|
84
|
+
cookies[Vanity.configuration.cookie_name] = cookie.call(@vanity_identity)
|
75
85
|
@vanity_identity
|
76
86
|
else # during functional testing
|
77
87
|
@vanity_identity = "test"
|
@@ -284,6 +294,44 @@ module Vanity
|
|
284
294
|
end
|
285
295
|
|
286
296
|
|
297
|
+
# When configuring use_js to true, you must set up a route to
|
298
|
+
# add_participant_route.
|
299
|
+
#
|
300
|
+
# Step 1: Add a new resource in config/routes.rb:
|
301
|
+
# post "/vanity/add_participant" => "vanity#add_participant"
|
302
|
+
#
|
303
|
+
# Step 2: Include Vanity::Rails::AddParticipant (or Vanity::Rails::Dashboard) in VanityController
|
304
|
+
# class VanityController < ApplicationController
|
305
|
+
# include Vanity::Rails::AddParticipant
|
306
|
+
# end
|
307
|
+
#
|
308
|
+
# Step 3: Open your browser to http://localhost:3000/vanity
|
309
|
+
module AddParticipant
|
310
|
+
# JS callback action used by vanity_js
|
311
|
+
def add_participant
|
312
|
+
if params[:v].nil?
|
313
|
+
render :status => 404, :nothing => true
|
314
|
+
return
|
315
|
+
end
|
316
|
+
|
317
|
+
h = {}
|
318
|
+
params[:v].split(',').each do |pair|
|
319
|
+
exp_id, answer = pair.split('=')
|
320
|
+
exp = Vanity.playground.experiment(exp_id.to_s.to_sym) rescue nil
|
321
|
+
answer = answer.to_i
|
322
|
+
|
323
|
+
if !exp || !exp.alternatives[answer]
|
324
|
+
render :status => 404, :nothing => true
|
325
|
+
return
|
326
|
+
end
|
327
|
+
h[exp] = exp.alternatives[answer].value
|
328
|
+
end
|
329
|
+
|
330
|
+
h.each{ |e,a| e.chooses(a, request) }
|
331
|
+
render :status => 200, :nothing => true
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
287
335
|
# Step 1: Add a new resource in config/routes.rb:
|
288
336
|
# map.vanity "/vanity/:action/:id", :controller=>:vanity
|
289
337
|
#
|
@@ -320,6 +368,18 @@ module Vanity
|
|
320
368
|
end
|
321
369
|
end
|
322
370
|
|
371
|
+
def disable
|
372
|
+
exp = Vanity.playground.experiment(params[:e].to_sym)
|
373
|
+
exp.enabled = false
|
374
|
+
render :file=>Vanity.template("_experiment"), :locals=>{:experiment=>exp}
|
375
|
+
end
|
376
|
+
|
377
|
+
def enable
|
378
|
+
exp = Vanity.playground.experiment(params[:e].to_sym)
|
379
|
+
exp.enabled = true
|
380
|
+
render :file=>Vanity.template("_experiment"), :locals=>{:experiment=>exp}
|
381
|
+
end
|
382
|
+
|
323
383
|
def chooses
|
324
384
|
exp = Vanity.playground.experiment(params[:e].to_sym)
|
325
385
|
exp.chooses(exp.alternatives[params[:a].to_i].value)
|
@@ -333,29 +393,7 @@ module Vanity
|
|
333
393
|
render :file=>Vanity.template("_experiment"), :locals=>{:experiment=>exp}
|
334
394
|
end
|
335
395
|
|
336
|
-
|
337
|
-
def add_participant
|
338
|
-
if params[:v].nil?
|
339
|
-
render :status => 404, :nothing => true
|
340
|
-
return
|
341
|
-
end
|
342
|
-
|
343
|
-
h = {}
|
344
|
-
params[:v].split(',').each do |pair|
|
345
|
-
exp_id, answer = pair.split('=')
|
346
|
-
exp = Vanity.playground.experiment(exp_id.to_s.to_sym) rescue nil
|
347
|
-
answer = answer.to_i
|
348
|
-
|
349
|
-
if !exp || !exp.alternatives[answer]
|
350
|
-
render :status => 404, :nothing => true
|
351
|
-
return
|
352
|
-
end
|
353
|
-
h[exp] = exp.alternatives[answer].value
|
354
|
-
end
|
355
|
-
|
356
|
-
h.each{ |e,a| e.chooses(a, request) }
|
357
|
-
render :status => 200, :nothing => true
|
358
|
-
end
|
396
|
+
include AddParticipant
|
359
397
|
end
|
360
398
|
|
361
399
|
module TrackingImage
|
@@ -1,5 +1,8 @@
|
|
1
1
|
en:
|
2
2
|
vanity:
|
3
|
+
act_on_this_experiment:
|
4
|
+
enable: 'Enable this experiment'
|
5
|
+
disable: 'Disable this experiment'
|
3
6
|
best_alternative: '(%{probability}% this is best)'
|
4
7
|
best_alternative_is_significant: 'With %{probability}% probability this result is statistically significant.'
|
5
8
|
best_alternative_probability: 'With %{probability}% probability this result is the best.'
|
@@ -14,8 +17,14 @@ en:
|
|
14
17
|
converted: '%{count} converted'
|
15
18
|
converted_percentage: '%{alternative} converted at %{percentage}%.'
|
16
19
|
currently_shown: 'currently shown'
|
20
|
+
default: '(Default)'
|
17
21
|
didnt_convert: '%{alternative} did not convert.'
|
22
|
+
disable: 'Disable'
|
23
|
+
disabled: 'Disabled'
|
24
|
+
enable: 'Enable'
|
25
|
+
enabled: 'Enabled'
|
18
26
|
experiment_has_been_reset: '%{name} experiment has been reset.'
|
27
|
+
experiment_has_been_disabled: 'This experiment is currently disabled, and will always choose the default %{name}.'
|
19
28
|
experiment_participants:
|
20
29
|
one: 'There is one participant in this experiment.'
|
21
30
|
other: 'There are %{count} participants in this experiment.'
|
@@ -17,6 +17,10 @@ pt-BR:
|
|
17
17
|
converted_percentage: '%{alternative} converteu %{percentage}%'
|
18
18
|
currently_shown: 'atualmente exibido'
|
19
19
|
didnt_convert: '%{alternative} não converteu.'
|
20
|
+
disable: 'Desativar'
|
21
|
+
disabled: 'Desativado'
|
22
|
+
enable: 'Activar'
|
23
|
+
enabled: 'Activado'
|
20
24
|
experiment_participants:
|
21
25
|
one: 'Há um participante neste experimento.'
|
22
26
|
other: 'Há %{count} participantes neste experimento.'
|