vanity 2.0.1 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
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.'