Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

We’re showing branches in this repository, but you can also compare across forks.

base fork: lookout/vanity
base: dda319f280
...
head fork: lookout/vanity
compare: v1.6.2_lookout
  • 12 commits
  • 14 files changed
  • 0 commit comments
  • 1 contributor
1  .gitignore
View
@@ -6,3 +6,4 @@ test/myapp/log/
html
*.gem
.yardoc
+.idea
1  Gemfile
View
@@ -21,4 +21,5 @@ group :test do
gem "shoulda"
gem "timecop"
gem "webmock"
+ gem "ruby-debug"
end
14 Gemfile.lock
View
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
- vanity (1.6.0)
+ lookout-vanity (1.6.2)
redis (~> 2.0)
redis-namespace (~> 1.0.0)
@@ -26,6 +26,7 @@ GEM
bson_ext (1.3.1)
classifier (1.3.3)
fast-stemmer (>= 1.0.0)
+ columnize (0.3.4)
crack (0.1.8)
directory_watcher (1.4.0)
fast-stemmer (1.0.0)
@@ -41,6 +42,8 @@ GEM
liquid (>= 1.9.0)
maruku (>= 0.5.9)
kramdown (0.13.3)
+ linecache (0.46)
+ rbx-require-relative (> 0.0.4)
liquid (2.2.2)
maruku (0.6.0)
syntax (>= 1.0.0)
@@ -62,9 +65,15 @@ GEM
activesupport (= 2.3.12)
rake (>= 0.8.3)
rake (0.9.2)
+ rbx-require-relative (0.0.5)
redis (2.2.1)
redis-namespace (1.0.3)
redis (< 3.0.0)
+ ruby-debug (0.10.4)
+ columnize (>= 0.1)
+ ruby-debug-base (~> 0.10.4.0)
+ ruby-debug-base (0.10.4)
+ linecache (>= 0.3)
shoulda (2.11.3)
syntax (1.0.0)
timecop (0.3.5)
@@ -81,6 +90,7 @@ DEPENDENCIES
bson_ext
garb
jekyll
+ lookout-vanity!
mocha
mongo
mysql
@@ -88,8 +98,8 @@ DEPENDENCIES
rack
rails (~> 2.3.8)
rake
+ ruby-debug
shoulda
timecop
- vanity!
webmock
yard
1  lib/vanity.rb
View
@@ -26,6 +26,7 @@ module Vanity
require "vanity/experiment/ab_test"
# Database adapters
require "vanity/adapters/abstract_adapter"
+require "vanity/adapters/active_record_adapter"
require "vanity/adapters/redis_adapter"
require "vanity/adapters/mongodb_adapter"
require "vanity/adapters/mock_adapter"
5 lib/vanity/adapters/abstract_adapter.rb
View
@@ -105,6 +105,11 @@ def ab_add_participant(experiment, alternative, identity)
fail "Not implemented"
end
+ # Indicates which alternative has been picked for this participant. See #ab_add_participant.
+ def ab_chosen(experiment, identity)
+ false # TODO: default to false for now; should add to each adapter
+ end
+
# Records a conversion in this experiment for the given alternative.
# Associates a value with the conversion (default to 1). If implicit is
# true, add particpant if not already recorded for this experiment. If
77 lib/vanity/adapters/active_record_adapter.rb
View
@@ -23,7 +23,7 @@ def self.define_schema
# Migrate
unless VanitySchema.find_by_version(1)
connection.create_table :vanity_metrics do |t|
- t.string :metric_id
+ t.string :metric_id # name of the metric
t.datetime :updated_at
end
connection.add_index :vanity_metrics, [:metric_id]
@@ -37,7 +37,7 @@ def self.define_schema
connection.add_index :vanity_metric_values, [:vanity_metric_id]
connection.create_table :vanity_experiments do |t|
- t.string :experiment_id
+ t.string :experiment_id # name of the experiment
t.integer :outcome
t.datetime :created_at
t.datetime :completed_at
@@ -45,28 +45,45 @@ def self.define_schema
connection.add_index :vanity_experiments, [:experiment_id]
connection.create_table :vanity_conversions do |t|
- t.integer :vanity_experiment_id
+ t.references :vanity_experiment
t.integer :alternative
t.integer :conversions
end
- connection.add_index :vanity_conversions, [:vanity_experiment_id, :alternative], :name => "by_experiment_id_and_alternative"
+ connection.add_index :vanity_conversions, [:vanity_experiment_id, :alternative]
+
+ VanitySchema.create(:version => 1)
+ end
+
+ unless VanitySchema.find_by_version(2)
+ if connection.tables.include?('vanity_participants')
+ connection.drop_table :vanity_participants
+ end
connection.create_table :vanity_participants do |t|
- t.string :experiment_id
- t.string :identity
+ t.references :vanity_experiment
+ t.integer :identity
t.integer :shown
t.integer :seen
t.integer :converted
+ t.timestamps
end
- connection.add_index :vanity_participants, [:experiment_id]
- connection.add_index :vanity_participants, [:experiment_id, :identity], :name => "by_experiment_id_and_identity"
- connection.add_index :vanity_participants, [:experiment_id, :shown], :name => "by_experiment_id_and_shown"
- connection.add_index :vanity_participants, [:experiment_id, :seen], :name => "by_experiment_id_and_seen"
- connection.add_index :vanity_participants, [:experiment_id, :converted], :name => "by_experiment_id_and_converted"
+ connection.add_index :vanity_participants, [:vanity_experiment_id, :identity]
+ connection.add_index :vanity_participants, [:vanity_experiment_id, :shown]
+ connection.add_index :vanity_participants, [:vanity_experiment_id, :seen]
+ connection.add_index :vanity_participants, [:vanity_experiment_id, :converted]
- VanitySchema.create(:version => 1)
+ VanitySchema.create(:version => 2)
end
end
+
+ def self.drop_schema
+ connection.drop_table :vanity_schema
+ connection.drop_table :vanity_metrics
+ connection.drop_table :vanity_metric_values
+ connection.drop_table :vanity_experiments
+ connection.drop_table :vanity_conversions
+ connection.drop_table :vanity_participants
+ end
end
# Schema model
@@ -94,6 +111,7 @@ class VanityMetricValue < VanityRecord
class VanityExperiment < VanityRecord
set_table_name :vanity_experiments
has_many :vanity_conversions, :dependent => :destroy
+ has_many :vanity_participants
# Finds or creates the experiment
def self.retrieve(experiment)
@@ -115,6 +133,7 @@ class VanityConversion < VanityRecord
# Participant model
class VanityParticipant < VanityRecord
set_table_name :vanity_participants
+ belongs_to :vanity_experiment
# Finds the participant by experiment and identity. If
# create is true then it will create the participant
@@ -122,15 +141,17 @@ class VanityParticipant < VanityRecord
# passed to create if creating, or will be used to
# update the found participant.
def self.retrieve(experiment, identity, create = true, update_with = nil)
- record = VanityParticipant.first(
+ exp = VanityExperiment.retrieve(experiment)
+ raise RuntimeError, "no experiment found #{experiment}" unless exp
+ record = exp.vanity_participants.first(
:conditions =>
- {:experiment_id => experiment.to_s, :identity => identity.to_s})
+ {:identity => identity})
if record
record.update_attributes(update_with) if update_with
elsif create
record = VanityParticipant.create(
- {:experiment_id => experiment.to_s,
+ {:vanity_experiment_id => exp.id,
:identity => identity}.merge(update_with || {}))
end
@@ -139,10 +160,11 @@ def self.retrieve(experiment, identity, create = true, update_with = nil)
end
def initialize(options)
- options[:adapter] = options[:active_record_adapter] if options[:active_record_adapter]
-
- VanityRecord.establish_connection(options)
- VanityRecord.define_schema
+ if options[:active_record_adapter] && (options[:active_record_adapter] != "default")
+ options[:adapter] = options[:active_record_adapter]
+ VanityRecord.establish_connection(options)
+ end
+ VanityRecord.define_schema unless options[:manual_migration]
end
def active?
@@ -238,8 +260,8 @@ def is_experiment_completed?(experiment)
# :conversions.
def ab_counts(experiment, alternative)
record = VanityExperiment.retrieve(experiment)
- participants = VanityParticipant.count(:conditions => {:experiment_id => experiment.to_s, :seen => alternative})
- converted = VanityParticipant.count(:conditions => {:experiment_id => experiment.to_s, :converted => alternative})
+ participants = record.vanity_participants.count(:conditions => {:seen => alternative})
+ converted = record.vanity_participants.count(:conditions => {:converted => alternative})
conversions = record.vanity_conversions.sum(:conversions, :conditions => {:alternative => alternative})
{
@@ -272,6 +294,12 @@ def ab_add_participant(experiment, alternative, identity)
VanityParticipant.retrieve(experiment, identity, true, :seen => alternative)
end
+ # Indicates which alternative has been picked for this participant. See #ab_add_participant.
+ def ab_chosen(experiment, identity)
+ participant = VanityParticipant.retrieve(experiment, identity, false)
+ participant && participant.seen
+ end
+
# Records a conversion in this experiment for the given alternative.
# Associates a value with the conversion (default to 1). If implicit is
# true, add particpant if not already recorded for this experiment. If
@@ -295,9 +323,10 @@ def ab_set_outcome(experiment, alternative = 0)
# Deletes all information about this experiment.
def destroy_experiment(experiment)
- VanityParticipant.delete_all(:experiment_id => experiment.to_s)
- record = VanityExperiment.find_by_experiment_id(experiment.to_s)
- record && record.destroy
+ if record = VanityExperiment.find_by_experiment_id(experiment.to_s)
+ record.vanity_participants.delete_all
+ record.destroy
+ end
end
end
end
63 lib/vanity/experiment/ab_test.rb
View
@@ -145,6 +145,10 @@ def metrics(*args)
# puts "#{alts.count} alternatives, with the colors: #{alts.map(&:value).join(", ")}"
def alternatives(*args)
@alternatives = args.empty? ? [true, false] : args.clone
+ if @control
+ @alternatives.delete(@control)
+ @alternatives.push(@control) # move to end
+ end
class << self
define_method :alternatives, instance_method(:_alternatives)
end
@@ -279,6 +283,44 @@ def showing?(alternative)
end
end
+ # True if this experiment has been selected for the current id (see #chooses).
+ def chosen?
+ # True if experiment is active and a value has been chosen for current identity
+ !!if @playground.collecting? # return a boolean value
+ active? && (connection.ab_showing(@id, identity()) ||
+ connection.ab_chosen(@id, identity()))
+ # TODO: implement ab_chosen on all vanity adapters!!
+ else
+ @showing && @showing[identity()]
+ end
+ end
+
+ def assign_on(event)
+ @assign_event = event
+ end
+
+ def assign_on?(event)
+ @assign_event == event
+ end
+
+ def test_percent(pct)
+ raise RuntimeError, "Test percent must be an integer" unless pct.kind_of?(Integer)
+ @test_pct = pct
+ end
+
+ def control_percent(pct)
+ raise RuntimeError, "Control percent must be an integer" unless pct.kind_of?(Integer)
+ test_percent(100-pct)
+ end
+
+ def control_value(value)
+ @control = value
+ if @alternatives
+ @alternatives.delete(@control)
+ @alternatives.push(@control) # move to end
+ end
+ end
+
# -- Reporting --
@@ -463,12 +505,12 @@ def fake(values)
participants.times do |identity|
index = @alternatives.index(value)
raise ArgumentError, "No alternative #{value.inspect} for #{name}" unless index
- connection.ab_add_participant @id, index, "#{index}:#{identity}"
+ connection.ab_add_participant @id, index, index*10000+identity #"#{index}:#{identity}"
end
conversions.times do |identity|
index = @alternatives.index(value)
raise ArgumentError, "No alternative #{value.inspect} for #{name}" unless index
- connection.ab_add_conversion @id, index, "#{index}:#{identity}"
+ connection.ab_add_conversion @id, index, index*10000+identity #"#{index}:#{identity}"
end
end
end
@@ -478,9 +520,24 @@ def fake(values)
# identity, and randomly distributed alternatives for each identity (in the
# same experiment).
def alternative_for(identity)
- Digest::MD5.hexdigest("#{name}/#{identity}").to_i(17) % @alternatives.size
+ id_hash = Digest::MD5.hexdigest("#{name}/#{identity}").to_i(16)
+ return hash_to_alternative(id_hash)
end
+ def hash_to_alternative(id_hash)
+ alternatives_count = @alternatives.size
+ if @test_pct
+ if (id_hash % 100 >= @test_pct)
+ return alternatives_count-1 #@control
+ else
+ id_hash = id_hash / 100
+ alternatives_count -= 1
+ end
+ end
+ return id_hash % alternatives_count
+ end
+
+
begin
a = 50.0
# Returns array of [z-score, percentage]
2  lib/vanity/experiment/base.rb
View
@@ -181,7 +181,7 @@ def identity
def default_identify(context)
raise "No Vanity.context" unless context
raise "Vanity.context does not respond to vanity_identity" unless context.respond_to?(:vanity_identity)
- context.vanity_identity or raise "Vanity.context.vanity_identity - no identity"
+ context.send(:vanity_identity) or raise "Vanity.context.vanity_identity - no identity"
end
# Derived classes call this after state changes that may lead to
8 lib/vanity/frameworks/rails.rb
View
@@ -57,6 +57,7 @@ def use_vanity(symbol = nil, &block)
end
end
end
+ protected :vanity_identity
around_filter :vanity_context_filter
before_filter :vanity_reload_filter unless ::Rails.configuration.cache_classes
before_filter :vanity_query_parameter_filter
@@ -143,6 +144,7 @@ module Helpers
# <% ab_test :features do |count| %>
# <%= count %> features to choose from!
# <% end %>
+ # FIXME: this appears to be duplicate code see ../helpers.rb
def ab_test(name, &block)
if Vanity.playground.using_js?
@_vanity_experiments ||= {}
@@ -262,11 +264,7 @@ class Plugin < Rails::Railtie # :nodoc:
if defined?(PhusionPassenger)
PhusionPassenger.on_event(:starting_worker_process) do |forked|
if forked
- begin
- Vanity.playground.establish_connection if Vanity.playground.collecting?
- rescue Exception=>ex
- Rails.logger.error "Error reconnecting: #{ex.to_s}"
- end
+ Vanity.playground.establish_connection if Vanity.playground.collecting?
end
end
end
5 lib/vanity/helpers.rb
View
@@ -50,6 +50,11 @@ def ab_test(name, &block)
end
end
+ # Check whether the specified experiment has had a value chosen yet
+ def ab_test_chosen?(id)
+ Vanity.playground.experiment(id).chosen?
+ end
+
# Tracks an action associated with a metric.
#
# @example
33 lib/vanity/playground.rb
View
@@ -193,7 +193,7 @@ def metrics
Dir[File.join(load_path, "metrics/*.rb")].each do |file|
Metric.load self, @loading, file
end
- if File.exist?("config/vanity.yml") && remote = YAML.load(ERB.new(File.read("config/vanity.yml")).result)["metrics"]
+ if config_file_exists? && load_config_file["metrics"]
remote.each do |id, url|
fail "Metric #{id} already defined in playground" if metrics[id.to_sym]
metric = Metric.new(self, id)
@@ -215,6 +215,17 @@ def track!(id, count = 1)
metric(id).track! count
end
+ # Choose a value for any experiment tagged with the specified event;
+ # filter can be used to specify a white-list for experiments.
+ def assign_on(event, filter=nil)
+ experiments.each do |id, obj|
+ if !filter || filter.include?(id)
+ if obj.assign_on?(event)
+ obj.choose
+ end
+ end
+ end
+ end
# -- Connection management --
@@ -245,21 +256,21 @@ def establish_connection(spec = nil)
disconnect! if @adapter
case spec
when nil
- if File.exists?("config/vanity.yml")
+ if config_file_exists?
env = ENV["RACK_ENV"] || ENV["RAILS_ENV"] || "development"
- spec = YAML.load(ERB.new(File.read("config/vanity.yml")).result)[env]
+ spec = load_config_file[env]
fail "No configuration for #{env}" unless spec
establish_connection spec
- elsif File.exists?("config/redis.yml")
+ elsif config_file_exists?("redis.yml")
env = ENV["RACK_ENV"] || ENV["RAILS_ENV"] || "development"
- redis = YAML.load(ERB.new(File.read("config/redis.yml")).result)[env]
+ redis = load_config_file("redis.yml")[env]
fail "No configuration for #{env}" unless redis
establish_connection "redis://" + redis
else
establish_connection :adapter=>"redis"
end
when Symbol
- spec = YAML.load(ERB.new(File.read("config/vanity.yml")).result)[spec.to_s]
+ spec = load_config_file[spec.to_s]
establish_connection spec
when String
uri = URI.parse(spec)
@@ -277,6 +288,16 @@ def establish_connection(spec = nil)
end
end
+ CONFIG_FILE_ROOT = (defined?(::Rails) ? ::Rails.root : Pathname.new(".")) + "config"
+
+ def config_file_exists?(basename = "vanity.yml")
+ File.exists?(CONFIG_FILE_ROOT + basename)
+ end
+
+ def load_config_file(basename = "vanity.yml")
+ YAML.load(ERB.new(File.read(CONFIG_FILE_ROOT + basename)).result)
+ end
+
# Returns the current connection. Establishes new connection is necessary.
#
# @since 1.4.0
2  lib/vanity/version.rb
View
@@ -1,5 +1,5 @@
module Vanity
- VERSION = "1.6.1"
+ VERSION = "1.6.2"
module Version
version = VERSION.to_s.split(".").map { |i| i.to_i }
2  vanity.gemspec → lookout-vanity.gemspec
View
@@ -2,7 +2,7 @@ $: << File.dirname(__FILE__) + "/lib"
require "vanity/version"
Gem::Specification.new do |spec|
- spec.name = "vanity"
+ spec.name = "lookout-vanity"
spec.version = Vanity::VERSION
spec.author = "Assaf Arkin"
spec.email = "assaf@labnotes.org"
2  test/test_helper.rb
View
@@ -14,6 +14,7 @@
require "lib/vanity"
require "timecop"
require "webmock/test_unit"
+require "ruby-debug"
if $VERBOSE
@@ -46,6 +47,7 @@ def new_playground
spec = {
"redis"=>"redis://localhost/15",
"mongodb"=>"mongodb://localhost/vanity-test",
+ "mysql"=>{:adapter=>"active_record", :active_record_adapter=>"mysql", :database=>"vanity_test"},
"mock"=>"mock:/"
}[adapter]
raise "No support yet for #{adapter}" unless spec

No commit comments for this range

Something went wrong with that request. Please try again.