vanity 2.0.1 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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.'
|