vanity 1.2.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. data/CHANGELOG +34 -0
  2. data/Gemfile +16 -0
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +10 -5
  5. data/Rakefile +119 -0
  6. data/bin/vanity +23 -18
  7. data/lib/vanity.rb +12 -4
  8. data/lib/vanity/commands.rb +1 -0
  9. data/lib/vanity/commands/list.rb +21 -0
  10. data/lib/vanity/experiment/ab_test.rb +8 -1
  11. data/lib/vanity/experiment/base.rb +40 -30
  12. data/lib/vanity/frameworks/rails.rb +222 -0
  13. data/lib/vanity/metric/active_record.rb +77 -0
  14. data/lib/vanity/{metric.rb → metric/base.rb} +6 -71
  15. data/lib/vanity/metric/google_analytics.rb +76 -0
  16. data/lib/vanity/playground.rb +93 -44
  17. data/lib/vanity/templates/_metric.erb +12 -7
  18. data/lib/vanity/templates/vanity.css +1 -0
  19. data/test/ab_test_test.rb +69 -48
  20. data/test/experiment_test.rb +29 -15
  21. data/test/metric_test.rb +104 -0
  22. data/test/myapp/app/controllers/application_controller.rb +2 -0
  23. data/test/myapp/app/controllers/main_controller.rb +7 -0
  24. data/test/myapp/config/boot.rb +110 -0
  25. data/test/myapp/config/environment.rb +10 -0
  26. data/test/myapp/config/environments/production.rb +0 -0
  27. data/test/myapp/config/routes.rb +3 -0
  28. data/test/myapp/log/production.log +80 -0
  29. data/test/passenger_test.rb +34 -0
  30. data/test/rails_test.rb +129 -1
  31. data/test/test_helper.rb +12 -4
  32. data/vanity.gemspec +2 -2
  33. data/vendor/cache/RedCloth-4.2.2.gem +0 -0
  34. data/vendor/cache/actionmailer-2.3.5.gem +0 -0
  35. data/vendor/cache/actionpack-2.3.5.gem +0 -0
  36. data/vendor/cache/activerecord-2.3.5.gem +0 -0
  37. data/vendor/cache/activeresource-2.3.5.gem +0 -0
  38. data/vendor/cache/activesupport-2.3.5.gem +0 -0
  39. data/vendor/cache/autotest-4.2.7.gem +0 -0
  40. data/vendor/cache/autotest-fsevent-0.2.1.gem +0 -0
  41. data/vendor/cache/autotest-growl-0.2.0.gem +0 -0
  42. data/vendor/cache/bundler-0.9.7.gem +0 -0
  43. data/vendor/cache/classifier-1.3.1.gem +0 -0
  44. data/vendor/cache/directory_watcher-1.3.1.gem +0 -0
  45. data/vendor/cache/fastthread-1.0.7.gem +0 -0
  46. data/vendor/cache/garb-0.7.0.gem +0 -0
  47. data/vendor/cache/happymapper-0.3.0.gem +0 -0
  48. data/vendor/cache/jekyll-0.5.7.gem +0 -0
  49. data/vendor/cache/libxml-ruby-1.1.3.gem +0 -0
  50. data/vendor/cache/liquid-2.0.0.gem +0 -0
  51. data/vendor/cache/maruku-0.6.0.gem +0 -0
  52. data/vendor/cache/mocha-0.9.8.gem +0 -0
  53. data/vendor/cache/open4-1.0.1.gem +0 -0
  54. data/vendor/cache/passenger-2.2.9.gem +0 -0
  55. data/vendor/cache/rack-1.0.1.gem +0 -0
  56. data/vendor/cache/rails-2.3.5.gem +0 -0
  57. data/vendor/cache/rake-0.8.7.gem +0 -0
  58. data/vendor/cache/rubygems-update-1.3.5.gem +0 -0
  59. data/vendor/cache/shoulda-2.10.3.gem +0 -0
  60. data/vendor/cache/sqlite3-ruby-1.2.5.gem +0 -0
  61. data/vendor/cache/stemmer-1.0.1.gem +0 -0
  62. data/vendor/cache/syntax-1.0.0.gem +0 -0
  63. data/vendor/cache/sys-uname-0.8.4.gem +0 -0
  64. data/vendor/cache/timecop-0.3.4.gem +0 -0
  65. metadata +60 -11
  66. data/lib/vanity/rails.rb +0 -22
  67. data/lib/vanity/rails/dashboard.rb +0 -24
  68. data/lib/vanity/rails/helpers.rb +0 -101
  69. data/lib/vanity/rails/testing.rb +0 -11
data/CHANGELOG CHANGED
@@ -1,3 +1,37 @@
1
+ == 1.3.0 (2010-03-01)
2
+ This release adds support for Google Analytics, AdWords and forking servers (Passenger, Unicorn).
3
+
4
+ To view Google Analytics metrics from within Vanity, first make sure you are using Garb. For example, in your Gemfile:
5
+
6
+ gem "vanity", "1.3.0"
7
+ gem "garb", "0.5.0"
8
+
9
+ Next, authenticate using your account credentials. For example, in your config/environments/production.rb:
10
+
11
+ require "garb"
12
+ Garb::Session.login('..email..', '..password..', account_type: "GOOGLE") rescue nil
13
+
14
+ Last, define Vanity metrics that tap to Google Analytics metrics. For example:
15
+
16
+ metric "Acquisition: Visitors" do
17
+ description "Unique visitors on any given page, as tracked by Google Analytics"
18
+ google_analytics "UA-1828623-6", :visitors
19
+ end
20
+
21
+ * Added: Support for Google Analytics metrics, thanks to Tony Pitale's Garb and blog post: http://www.viget.com/extend/user-goal-tracking-in-rails-with-vanity-and-google-analytics/
22
+ * Added: Vanity query parameter that you can use to choose a particular alternative, e.g. to tie an advertisement banner with content of the site.
23
+ * Added: Command line "vanity list" catalogs all ongoing experiments, their alternatives (and fingerprints) and all metrics.
24
+ * Added: Playground.reconnect!, particularly useful when forking (Passenger, Unicorn, etc).
25
+ * Added: Vanity loads Redis configuration from config/redis.yml (if you have such a file).
26
+ * Changed: New way to specify connection configuration: Vanity.playground.redis = "localhost:6379". Use this instead of the separate host/port/db attribute.
27
+ * Changed: Rails integration now separates use_vanity method, filters and helpers.
28
+ * Changed: Explicit vanity_context_filter and vanity_reload_filter so you can skip them, or order filters relative to them.
29
+ * Fixed: If metric cannot be loaded (e.g. offline, no db access) show error message for that metric but don't break dashboard.
30
+ * Fixed: AbTest incorrectly calls identify method instead of identity (issue #2)
31
+ * Fixed: Running vanity command, automatically detects and loads Rails.
32
+ * Fixed: Vanity now picks up on load_path set from within config/environment.rb.
33
+ * Removed: Vanity.playground.define is deprecated. Bad choice for a method name. If you need this feature, make a suggestion and let's create a better API.
34
+
1
35
  == 1.2.0 (2009-12-14)
2
36
  This release introduces metrics backed by ActiveRecord. Use them when your model is already tracking a metric, and you get instant historical data.
3
37
 
data/Gemfile ADDED
@@ -0,0 +1,16 @@
1
+ source "http://rubygems.org/"
2
+ gem "autotest"
3
+ gem "autotest-fsevent"
4
+ gem "autotest-growl"
5
+ gem "bundler"
6
+ gem "garb"
7
+ gem "jekyll"
8
+ gem "mocha"
9
+ gem "passenger"
10
+ gem "rails", "2.3.5"
11
+ gem "rack", "1.0.1"
12
+ gem "rubygems-update"
13
+ gem "shoulda"
14
+ gem "sqlite3-ruby"
15
+ gem "timecop"
16
+ gem "yard"
@@ -1,4 +1,4 @@
1
- Copyright (c) 2009 Assaf Arkin
1
+ Copyright (c) 2010 Assaf Arkin
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
@@ -11,7 +11,13 @@ http://farm3.static.flickr.com/2540/4099665871_497f274f68_o.jpg
11
11
 
12
12
  <b>Step 1:</b> Start using Vanity in your Rails application:
13
13
 
14
- gem.config "vanity"
14
+ Rails::Initializer.run do |config|
15
+ gem.config "vanity"
16
+
17
+ config.after_initialize do
18
+ require "vanity"
19
+ end
20
+ end
15
21
 
16
22
  And:
17
23
 
@@ -47,21 +53,20 @@ And:
47
53
 
48
54
  <b>Step 5:</b> Check the report:
49
55
 
50
- vanity --output vanity.html
56
+ vanity report --output vanity.html
51
57
 
52
58
 
53
59
  == Building From Source
54
60
 
55
61
  To run the test suite for the first time:
56
62
 
57
- $ gem install rails mocha timecop
58
- $ rake
63
+ $ bundle install
64
+ $ bundle exec rake
59
65
 
60
66
  You can also +rake test+ if you insist on being explicit.
61
67
 
62
68
  To build the documentation:
63
69
 
64
- $ gem install yardoc jekyll
65
70
  $ rake docs
66
71
  $ open html/index.html
67
72
 
@@ -0,0 +1,119 @@
1
+ require "rake/testtask"
2
+
3
+ spec = Gem::Specification.load(File.expand_path("vanity.gemspec", File.dirname(__FILE__)))
4
+
5
+ desc "Build the Gem"
6
+ task :build=>:test do
7
+ sh "gem build #{spec.name}.gemspec"
8
+ end
9
+
10
+ desc "Install #{spec.name} locally"
11
+ task :install=>:build do
12
+ sudo = "sudo" unless File.writable?( Gem::ConfigMap[:bindir])
13
+ sh "#{sudo} gem install #{spec.name}-#{spec.version}.gem"
14
+ end
15
+
16
+ desc "Push new release to gemcutter and git tag"
17
+ task :push=>:build do
18
+ sh "git push"
19
+ puts "Tagging version #{spec.version} .."
20
+ sh "git tag v#{spec.version}"
21
+ sh "git push --tag"
22
+ puts "Building and pushing gem .."
23
+ sh "gem push #{spec.name}-#{spec.version}.gem"
24
+ end
25
+
26
+
27
+ task :default=>:test
28
+ desc "Run all tests using Redis mock (also default task)"
29
+ Rake::TestTask.new do |task|
30
+ task.test_files = FileList['test/*_test.rb']
31
+ if Rake.application.options.trace
32
+ #task.warning = true
33
+ task.verbose = true
34
+ elsif Rake.application.options.silent
35
+ task.ruby_opts << "-W0"
36
+ else
37
+ task.verbose = true
38
+ end
39
+ end
40
+
41
+ desc "Run all tests using live redis server"
42
+ task "test:redis" do
43
+ ENV["REDIS"] = "true"
44
+ task(:test).invoke
45
+ end
46
+
47
+ task(:clobber) { rm_rf "tmp" }
48
+
49
+
50
+ begin
51
+ require "yard"
52
+ YARD::Rake::YardocTask.new(:yardoc) do |task|
53
+ task.files = FileList["lib/**/*.rb"].exclude("lib/vanity/backport.rb")
54
+ task.options = "--output", "html/api", "--title", "Vanity #{spec.version}", "--main", "README.rdoc", "--files", "CHANGELOG"
55
+ end
56
+ rescue LoadError
57
+ end
58
+
59
+ desc "Jekyll generates the main documentation (sans API)"
60
+ task(:jekyll) { sh "jekyll", "doc", "html" }
61
+
62
+ desc "Create documentation in docs directory (including API)"
63
+ task :docs=>[:jekyll, :yardoc]
64
+ desc "Remove temporary files and directories"
65
+ task(:clobber) { rm_rf "html" }
66
+
67
+ desc "Publish documentation to vanity.labnotes.org"
68
+ task :publish=>[:clobber, :docs] do
69
+ sh "rsync -cr --del --progress html/ labnotes.org:/var/www/vanity/"
70
+ end
71
+
72
+
73
+ task :report do
74
+ $LOAD_PATH.unshift "lib"
75
+ require "vanity"
76
+ require "timecop"
77
+ Vanity.playground.load_path = "test/experiments"
78
+ Vanity.playground.experiments.values.each(&:destroy)
79
+ Vanity.playground.metrics.values.each(&:destroy!)
80
+ Vanity.playground.reload!
81
+
82
+ # Control 182 35 19.23% N/A
83
+ # Treatment A 180 45 25.00% 1.33
84
+ # Treatment B 189 28 14.81% -1.13
85
+ # Treatment C 188 61 32.45% 2.94
86
+ Vanity.playground.experiment(:null_abc).instance_eval do
87
+ fake nil=>[182,35], :red=>[180,45], :green=>[189,28], :blue=>[188,61]
88
+ @created_at = (Date.today - 40).to_time
89
+ @completed_at = (Date.today - 35).to_time
90
+ end
91
+
92
+ Vanity.playground.experiment(:age_and_zipcode).instance_eval do
93
+ fake false=>[80,35], true=>[84,32]
94
+ @created_at = (Date.today - 30).to_time
95
+ @completed_at = (Date.today - 15).to_time
96
+ end
97
+
98
+ Vanity.context = Object.new
99
+ Vanity.context.instance_eval { def vanity_identity ; 0 ; end }
100
+ signups = 50
101
+ (Date.today - 90..Date.today).each do |date|
102
+ Timecop.travel date do
103
+ signups += rand(15) - 5
104
+ Vanity.playground.track! :signups, signups
105
+ end
106
+ end
107
+
108
+ cheers, yawns = 0, 0
109
+ (Date.today - 80..Date.today).each do |date|
110
+ Timecop.travel date do
111
+ cheers = cheers - 5 + rand(20)
112
+ Vanity.playground.track! :yawns, cheers
113
+ yawns = yawns - 5 + rand(30)
114
+ Vanity.playground.track! :cheers, yawns
115
+ end
116
+ end
117
+
118
+ Vanity::Commands.report ENV["OUTPUT"]
119
+ end
data/bin/vanity CHANGED
@@ -1,8 +1,13 @@
1
1
  #!/usr/bin/env ruby
2
- path = File.expand_path("../lib", File.dirname(__FILE__))
3
- $LOAD_PATH.unshift path unless $LOAD_PATH.include?(path)
2
+ # Is there a better way to detect Rails?
3
+ if File.exist?("config/boot.rb") && File.exist?("config/environment.rb")
4
+ require "config/environment"
5
+ else
6
+ path = File.expand_path("../lib", File.dirname(__FILE__))
7
+ $LOAD_PATH.unshift path unless $LOAD_PATH.include?(path)
8
+ require "vanity"
9
+ end
4
10
 
5
- require "vanity"
6
11
  require "optparse"
7
12
 
8
13
  playground = Vanity.playground
@@ -10,24 +15,24 @@ options = Struct.new(:output).new
10
15
  opts = OptionParser.new("", 24, " ") do |opts|
11
16
  opts.banner = "Usage: #{File.basename($0)} [options] command\n"
12
17
  opts.banner << "Commands:\n"
13
- opts.banner << " report Report on all running experiments"
14
-
15
- opts.separator ""
16
- opts.separator "General options:"
17
- opts.on("--path PATH", "Path to experiments directory (default: #{playground.load_path})") { |v| playground.load_path = v }
18
- opts.on("--output FILE", "Write report to this file (default: stdout)") { |v| options.output = v }
18
+ opts.banner << " list List all experiments and metrics\n"
19
+ opts.banner << " report Report on all running experiments/metrics\n"
19
20
 
20
21
  opts.separator ""
21
- opts.separator "Redis options:"
22
- opts.on("--host HOST", "Redis server host (default: #{playground.host})") { |v| playground.host = v }
23
- opts.on("--port PORT", "Redis server port (default: #{playground.port})") { |v| playground.port = v }
24
- opts.on("--db DB", "Redis database (default: #{playground.db})") { |v| playground.db = v }
25
- opts.on("--password PWD", "Redis database password") { |v| playground.password = v }
26
- opts.on("--namespace NS", "Redis namespace (default: #{playground.namespace})") { |v| playground.namespace = v }
22
+ opts.separator "Reporting options:"
23
+ opts.on "--output FILE", "Write report to this file (default: stdout)" do |path|
24
+ options.output = path
25
+ end
27
26
 
28
27
  opts.separator ""
29
28
  opts.separator "Common options:"
30
- opts.on_tail "-h", "-H", "--help", "Show this message" do
29
+ opts.on "--load_path PATH", "Path to experiments directory (default: #{playground.load_path})" do |path|
30
+ playground.load_path = path
31
+ end
32
+ opts.on "--redis HOST:PORT:DB", "Redis server host (default: localhost:6379)" do |redis|
33
+ playground.redis = redis
34
+ end
35
+ opts.on_tail "-h", "--help", "Show this message" do
31
36
  puts opts.to_s.gsub(/^.*DEPRECATED.*$/s, '')
32
37
  exit
33
38
  end
@@ -45,8 +50,8 @@ end
45
50
 
46
51
  ARGV.each do |cmd|
47
52
  case cmd
48
- when "report"
49
- Vanity::Commands.report options.output
53
+ when "report"; Vanity::Commands.report options.output
54
+ when "list"; Vanity::Commands.list
50
55
  else fail "No such command: #{cmd}"
51
56
  end
52
57
  end
@@ -1,9 +1,10 @@
1
- $LOAD_PATH.unshift File.join(File.dirname(__FILE__), "../vendor/redis-rb/lib")
2
- require "redis"
3
1
  require "date"
4
2
  require "time"
5
3
  require "logger"
6
4
 
5
+ $LOAD_PATH << File.join(File.dirname(__FILE__), "../vendor/redis-rb/lib")
6
+ autoload :Redis, "redis"
7
+
7
8
  # All the cool stuff happens in other places.
8
9
  # @see Vanity::Helper
9
10
  # @see Vanity::Rails
@@ -19,14 +20,21 @@ module Vanity
19
20
  PATCH = version[2]
20
21
  STRING = "#{MAJOR}.#{MINOR}.#{PATCH}"
21
22
  end
23
+
22
24
  end
23
25
 
24
26
  require "vanity/backport" if RUBY_VERSION < "1.9"
25
- require "vanity/metric"
27
+ # Metrics.
28
+ require "vanity/metric/base"
29
+ require "vanity/metric/active_record"
30
+ require "vanity/metric/google_analytics"
31
+ # Experiments.
26
32
  require "vanity/experiment/base"
27
33
  require "vanity/experiment/ab_test"
34
+ # Playground.
28
35
  require "vanity/playground"
29
36
  require "vanity/helpers"
30
37
  Vanity.autoload :MockRedis, "vanity/mock_redis"
31
38
  Vanity.autoload :Commands, "vanity/commands"
32
- require "vanity/rails" if defined?(Rails)
39
+ # Integration with various frameworks.
40
+ require "vanity/frameworks/rails" if defined?(Rails)
@@ -1 +1,2 @@
1
1
  require "vanity/commands/report"
2
+ require "vanity/commands/list"
@@ -0,0 +1,21 @@
1
+ module Vanity
2
+ module Commands
3
+ class << self
4
+ # Lists all experiments and metrics.
5
+ def list
6
+ Vanity.playground.experiments.each do |id, experiment|
7
+ puts "experiment :%-.20s (%-.40s)" % [id, experiment.name]
8
+ if experiment.respond_to?(:alternatives)
9
+ experiment.alternatives.each do |alt|
10
+ hash = experiment.fingerprint(alt)
11
+ puts " %s: %-40.40s (%s)" % [alt.name, alt.value, hash]
12
+ end
13
+ end
14
+ end
15
+ Vanity.playground.metrics.each do |id, metric|
16
+ puts "metric :%-.20s (%-.40s)" % [id, metric.name]
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -195,11 +195,18 @@ module Vanity
195
195
  check_completion!
196
196
  end
197
197
  else
198
- index = redis[key("outcome")] || alternative_for(identify)
198
+ index = redis[key("outcome")] || alternative_for(identity)
199
199
  end
200
200
  @alternatives[index.to_i]
201
201
  end
202
202
 
203
+ # Returns fingerprint (hash) for given alternative. Can be used to lookup
204
+ # alternative for experiment without revealing what values are available
205
+ # (e.g. choosing alternative from HTTP query parameter).
206
+ def fingerprint(alternative)
207
+ Digest::MD5.hexdigest("#{id}:#{alternative.id}")[-10,10]
208
+ end
209
+
203
210
 
204
211
  # -- Testing --
205
212
 
@@ -15,11 +15,16 @@ module Vanity
15
15
  # Defines a new experiment, given the experiment's name, type and
16
16
  # definition block.
17
17
  def define(name, type, options = nil, &block)
18
- playground.define(name, type, options || {}, &block)
18
+ fail "Experiment #{@experiment_id} already defined in playground" if playground.experiments[@experiment_id]
19
+ klass = Experiment.const_get(type.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase })
20
+ experiment = klass.new(playground, @experiment_id, name, options)
21
+ experiment.instance_eval &block
22
+ experiment.save
23
+ playground.experiments[@experiment_id] = experiment
19
24
  end
20
25
 
21
- def binding_with(playground)
22
- @playground = playground
26
+ def new_binding(playground, id)
27
+ @playground, @experiment_id = playground, id
23
28
  binding
24
29
  end
25
30
 
@@ -37,16 +42,16 @@ module Vanity
37
42
  end
38
43
 
39
44
  # Playground uses this to load experiment definitions.
40
- def load(playground, stack, path, id)
41
- fn = File.join(path, "#{id}.rb")
42
- fail "Circular dependency detected: #{stack.join('=>')}=>#{fn}" if stack.include?(fn)
43
- source = File.read(fn)
44
- stack.push fn
45
+ def load(playground, stack, file)
46
+ fail "Circular dependency detected: #{stack.join('=>')}=>#{file}" if stack.include?(file)
47
+ source = File.read(file)
48
+ stack.push file
49
+ id = File.basename(file, ".rb").downcase.gsub(/\W/, "_").to_sym
45
50
  context = Object.new
46
51
  context.instance_eval do
47
52
  extend Definition
48
- experiment = eval(source, context.binding_with(playground), fn)
49
- fail NameError.new("Expected #{fn} to define experiment #{id}", id) unless experiment.id == id
53
+ experiment = eval(source, context.new_binding(playground, id), file)
54
+ fail NameError.new("Expected #{file} to define experiment #{id}", id) unless playground.experiments[id]
50
55
  experiment
51
56
  end
52
57
  rescue
@@ -59,12 +64,12 @@ module Vanity
59
64
 
60
65
  end
61
66
 
62
- def initialize(playground, id, name, options, &block)
67
+ def initialize(playground, id, name, options = nil)
63
68
  @playground = playground
64
69
  @id, @name = id.to_sym, name
65
70
  @options = options || {}
66
71
  @namespace = "#{@playground.namespace}:#{@id}"
67
- @identify_block = lambda { |context| context.vanity_identity }
72
+ @identify_block = method(:default_identify)
68
73
  end
69
74
 
70
75
  # Human readable experiment name (first argument you pass when creating a
@@ -101,14 +106,10 @@ module Vanity
101
106
  # end
102
107
  # end
103
108
  def identify(&block)
109
+ fail "Missing block" unless block
104
110
  @identify_block = block
105
111
  end
106
112
 
107
- def identity
108
- @identify_block.call(Vanity.context)
109
- end
110
- protected :identity
111
-
112
113
 
113
114
  # -- Reporting --
114
115
 
@@ -136,19 +137,6 @@ module Vanity
136
137
  @complete_block = block
137
138
  end
138
139
 
139
- # Derived classes call this after state changes that may lead to
140
- # experiment completing.
141
- def check_completion!
142
- if @complete_block
143
- begin
144
- complete! if @complete_block.call
145
- rescue
146
- # TODO: logging
147
- end
148
- end
149
- end
150
- protected :check_completion!
151
-
152
140
  # Force experiment to complete.
153
141
  def complete!
154
142
  redis.setnx key(:completed_at), Time.now.to_i
@@ -184,6 +172,28 @@ module Vanity
184
172
 
185
173
  protected
186
174
 
175
+ def identity
176
+ @identify_block.call(Vanity.context)
177
+ end
178
+
179
+ def default_identify(context)
180
+ raise "No Vanity.context" unless context
181
+ raise "Vanity.context does not respond to vanity_identity" unless context.respond_to?(:vanity_identity)
182
+ context.vanity_identity or raise "Vanity.context.vanity_identity - no identity"
183
+ end
184
+
185
+ # Derived classes call this after state changes that may lead to
186
+ # experiment completing.
187
+ def check_completion!
188
+ if @complete_block
189
+ begin
190
+ complete! if @complete_block.call
191
+ rescue
192
+ # TODO: logging
193
+ end
194
+ end
195
+ end
196
+
187
197
  # Returns key for this experiment, or with an argument, return a key
188
198
  # using the experiment as the namespace. Examples:
189
199
  # key => "vanity:experiments:green_button"