vanity 1.2.0 → 1.3.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 (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"