Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also compare across forks.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, 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
View
1  .gitignore
@@ -6,3 +6,4 @@ test/myapp/log/
html
*.gem
.yardoc
+.idea
View
1  Gemfile
@@ -21,4 +21,5 @@ group :test do
gem "shoulda"
gem "timecop"
gem "webmock"
+ gem "ruby-debug"
end
View
14 Gemfile.lock
@@ -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
View
1  lib/vanity.rb
@@ -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"
View
5 lib/vanity/adapters/abstract_adapter.rb
@@ -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
View
77 lib/vanity/adapters/active_record_adapter.rb
@@ -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
View
63 lib/vanity/experiment/ab_test.rb
@@ -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]
View
2  lib/vanity/experiment/base.rb
@@ -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
View
8 lib/vanity/frameworks/rails.rb
@@ -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
View
5 lib/vanity/helpers.rb
@@ -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
View
33 lib/vanity/playground.rb
@@ -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
View
2  lib/vanity/version.rb
@@ -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 }
View
2  vanity.gemspec → lookout-vanity.gemspec
@@ -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"
View
2  test/test_helper.rb
@@ -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.