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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -2
  3. data/Appraisals +6 -6
  4. data/CHANGELOG +9 -3
  5. data/Gemfile.lock +1 -1
  6. data/README.md +299 -0
  7. data/doc/configuring.textile +8 -1
  8. data/doc/identity.textile +2 -0
  9. data/doc/metrics.textile +10 -0
  10. data/gemfiles/rails32.gemfile.lock +1 -1
  11. data/gemfiles/rails41.gemfile.lock +1 -1
  12. data/gemfiles/rails42.gemfile.lock +1 -1
  13. data/gemfiles/{rails4.gemfile → rails42_protected_attributes.gemfile} +2 -2
  14. data/gemfiles/rails42_protected_attributes.gemfile.lock +209 -0
  15. data/lib/generators/templates/vanity_migration.rb +1 -0
  16. data/lib/vanity/adapters/abstract_adapter.rb +11 -0
  17. data/lib/vanity/adapters/active_record_adapter.rb +15 -1
  18. data/lib/vanity/adapters/mock_adapter.rb +14 -0
  19. data/lib/vanity/adapters/mongodb_adapter.rb +14 -0
  20. data/lib/vanity/adapters/redis_adapter.rb +15 -0
  21. data/lib/vanity/configuration.rb +43 -11
  22. data/lib/vanity/experiment/ab_test.rb +145 -15
  23. data/lib/vanity/experiment/alternative.rb +4 -0
  24. data/lib/vanity/frameworks/rails.rb +69 -31
  25. data/lib/vanity/locales/vanity.en.yml +9 -0
  26. data/lib/vanity/locales/vanity.pt-BR.yml +4 -0
  27. data/lib/vanity/metric/active_record.rb +9 -1
  28. data/lib/vanity/templates/_ab_test.erb +9 -2
  29. data/lib/vanity/templates/_experiment.erb +21 -1
  30. data/lib/vanity/templates/vanity.css +11 -3
  31. data/lib/vanity/templates/vanity.js +35 -6
  32. data/lib/vanity/version.rb +1 -1
  33. data/test/commands/report_test.rb +1 -0
  34. data/test/dummy/config/application.rb +1 -0
  35. data/test/experiment/ab_test.rb +414 -0
  36. data/test/experiment/base_test.rb +16 -10
  37. data/test/frameworks/rails/action_controller_test.rb +14 -6
  38. data/test/frameworks/rails/action_mailer_test.rb +8 -6
  39. data/test/frameworks/rails/action_view_test.rb +1 -0
  40. data/test/helper_test.rb +2 -0
  41. data/test/metric/active_record_test.rb +56 -0
  42. data/test/playground_test.rb +3 -0
  43. data/test/test_helper.rb +28 -2
  44. data/test/web/rails/dashboard_test.rb +2 -0
  45. data/vanity.gemspec +2 -2
  46. metadata +8 -8
  47. data/README.rdoc +0 -231
  48. 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.increment!(:conversions, count)
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,
@@ -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
- request.env &&
31
- request.env["HTTP_USER_AGENT"] &&
32
- request.env["HTTP_USER_AGENT"].match(/\(.*https?:\/\/.*\)/)
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
- config_path: File.join(Pathname.new("."), "config"),
50
- config_file: "vanity.yml",
51
- environment: ENV["RACK_ENV"] || ENV["RAILS_ENV"] || "development"
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, since well behaved bots typically
105
- # include a reference URI in their user agent strings. (Original idea:
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
- @alternatives ||= args.empty? ? [true, false] : args.clone
63
- @alternatives.each_with_index.map do |value, i|
64
- Alternative.new(self, i, value)
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
- identity = identity()
130
- index = connection.ab_showing(@id, identity) || connection.ab_assigned(@id, identity)
131
- unless index
132
- index = alternative_for(identity).to_i
133
- save_assignment_if_valid_visitor(identity, index, request) unless @playground.using_js?
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 @id, outcome || 0
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 @id
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 @id
443
- connection.set_experiment_created_at @id, Time.now
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]
@@ -88,6 +88,10 @@ module Vanity
88
88
  @participants = @converted = @conversions = 0
89
89
  end
90
90
  end
91
+
92
+ def default?
93
+ @experiment.default == self
94
+ end
91
95
  end
92
96
  end
93
97
  end
@@ -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["vanity_id"] = { :value=>@vanity_identity, :expires=>1.month.from_now }
76
+ cookies[Vanity.configuration.cookie_name] = cookie.call(@vanity_identity)
64
77
  @vanity_identity
65
- elsif cookies["vanity_id"]
66
- @vanity_identity = cookies["vanity_id"]
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["vanity_id"] || SecureRandom.hex(16)
71
- cookie = { :value=>@vanity_identity, :expires=>1.month.from_now }
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
- # JS callback action used by vanity_js
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.'