guinea_pig 0.1.3
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.
- data/.gitignore +19 -0
- data/.rvmrc.example +1 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +48 -0
- data/Rakefile +11 -0
- data/guinea_pig.gemspec +22 -0
- data/lib/generators/guinea_pig/migration_generator.rb +25 -0
- data/lib/generators/guinea_pig/templates/create_ab_tests.rb +17 -0
- data/lib/guinea_pig.rb +21 -0
- data/lib/guinea_pig/ab_test.rb +23 -0
- data/lib/guinea_pig/experiments.rb +15 -0
- data/lib/guinea_pig/version.rb +3 -0
- data/test/db/.keep +0 -0
- data/test/etc/test_session.rb +13 -0
- data/test/fixtures/ab_experiments.yml +8 -0
- data/test/guinea_pig_test.rb +63 -0
- data/test/models.rb +2 -0
- data/test/schema.rb +4 -0
- data/test/test_helper.rb +24 -0
- metadata +118 -0
data/.gitignore
ADDED
data/.rvmrc.example
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm use --create 1.9.3-p286@guinea_pig
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Fernando Guillen
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# GuineaPig
|
2
|
+
|
3
|
+
Very simple ABTest functionality for Ruby, based in ActiveRecord and with Rails 3 generators
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem "guinea_pig"
|
10
|
+
|
11
|
+
## Usage
|
12
|
+
|
13
|
+
### Configure the experiments
|
14
|
+
|
15
|
+
# config/ab_experiments.yml
|
16
|
+
experiment_monkey:
|
17
|
+
- "alternative_monkey_1"
|
18
|
+
- "alternative_monkey_2"
|
19
|
+
|
20
|
+
experiment_elefant:
|
21
|
+
- "alternative_elefant_1"
|
22
|
+
- "alternative_elefant_2"
|
23
|
+
- "alternative_elefant_3"
|
24
|
+
|
25
|
+
### Create the table
|
26
|
+
|
27
|
+
rails generate guinea_pig:migration
|
28
|
+
rake db:migrate
|
29
|
+
|
30
|
+
### Experiment!
|
31
|
+
|
32
|
+
#### LandingPage experiment
|
33
|
+
|
34
|
+
redirect_to GuineaPig.alternative(:experiment_monkey, user)
|
35
|
+
|
36
|
+
#### CSS experiment
|
37
|
+
|
38
|
+
<%= stylesheet_link_tag "/assets/css/#{GuineaPig.alternative(:experiment_monkey, user)}.css" %>
|
39
|
+
|
40
|
+
### Convert!
|
41
|
+
|
42
|
+
if user.has_bought_something?
|
43
|
+
GuineaPig.conversion(:experiment_monkey, user)
|
44
|
+
end
|
45
|
+
|
46
|
+
## Sate of the art
|
47
|
+
|
48
|
+
Beta version but already used in production environments
|
data/Rakefile
ADDED
data/guinea_pig.gemspec
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path("../lib/guinea_pig/version", __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Fernando Guillen"]
|
6
|
+
gem.email = ["fguillen.mail@gmail.com"]
|
7
|
+
gem.description = "Very simple ABTest functionality for Ruby, based in ActiveRecord and with Rails 3 generators"
|
8
|
+
gem.summary = "Very simple ABTest functionality for Ruby, based in ActiveRecord and with Rails 3 generators"
|
9
|
+
gem.homepage = "https://github.com/fguillen/GuineaPig"
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = "guinea_pig"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = GuineaPig::VERSION
|
17
|
+
|
18
|
+
gem.add_runtime_dependency "rails", "~> 3.0"
|
19
|
+
gem.add_development_dependency "mocha"
|
20
|
+
gem.add_development_dependency "sqlite3"
|
21
|
+
gem.add_development_dependency "assert_difference"
|
22
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require "rails/generators"
|
2
|
+
|
3
|
+
class GuineaPig::MigrationGenerator < Rails::Generators::Base
|
4
|
+
include Rails::Generators::Migration
|
5
|
+
|
6
|
+
desc "Generates migration for ABTest model"
|
7
|
+
|
8
|
+
def self.source_root
|
9
|
+
File.join(File.dirname(__FILE__), "templates")
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.next_migration_number(dirname) #:nodoc:
|
13
|
+
migration_number_attempt = Time.now.utc.strftime("%Y%m%d%H%M%S")
|
14
|
+
|
15
|
+
if ActiveRecord::Base.timestamped_migrations && migration_number_attempt > current_migration_number(dirname).to_s
|
16
|
+
migration_number_attempt
|
17
|
+
else
|
18
|
+
serial_migration_number(dirname)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def copy_migration
|
23
|
+
migration_template "create_ab_tests.rb", "db/migrate/create_ab_tests.rb"
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class CreateAbTests < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :ab_tests do |t|
|
4
|
+
t.string :experiment, :null => false
|
5
|
+
t.string :alternative, :null => false
|
6
|
+
t.integer :guinea_pig_id, :null => false
|
7
|
+
t.string :guinea_pig_type, :null => false
|
8
|
+
t.integer :seen_count, :default => 0
|
9
|
+
t.integer :conversion_count, :default => 0
|
10
|
+
t.timestamps
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.down
|
15
|
+
drop_table :ab_tests
|
16
|
+
end
|
17
|
+
end
|
data/lib/guinea_pig.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require "active_record"
|
2
|
+
require_relative "guinea_pig/version"
|
3
|
+
require_relative "guinea_pig/ab_test"
|
4
|
+
require_relative "guinea_pig/experiments"
|
5
|
+
|
6
|
+
module GuineaPig
|
7
|
+
def self.get(experiment, guinea_pig)
|
8
|
+
::GuineaPig::ABTest.where(:experiment => experiment, :guinea_pig_id => guinea_pig.id, :guinea_pig_type => guinea_pig.class.name).first_or_create
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.alternative(experiment, guinea_pig)
|
12
|
+
ab_test = get(experiment, guinea_pig)
|
13
|
+
ab_test.increment!(:seen_count)
|
14
|
+
ab_test.alternative
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.conversion(experiment, guinea_pig)
|
18
|
+
ab_test = get(experiment, guinea_pig)
|
19
|
+
ab_test.conversion!
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module GuineaPig
|
2
|
+
class ABTest < ::ActiveRecord::Base
|
3
|
+
before_validation :initialize_alternative, :on => :create
|
4
|
+
|
5
|
+
attr_accessible :experiment, :guinea_pig, :alternative
|
6
|
+
|
7
|
+
validates :alternative, :presence => true
|
8
|
+
validates :guinea_pig_id, :presence => true
|
9
|
+
validates :guinea_pig_type, :presence => true
|
10
|
+
validates :experiment, :presence => true
|
11
|
+
validates :experiment, :uniqueness => {:scope => [:guinea_pig_id, :guinea_pig_type]}
|
12
|
+
|
13
|
+
belongs_to :guinea_pig, :polymorphic => true
|
14
|
+
|
15
|
+
def conversion!
|
16
|
+
increment!(:conversion_count)
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize_alternative
|
20
|
+
self.alternative ||= ::GuineaPig::Experiments.pick_alternative(experiment)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module GuineaPig
|
2
|
+
module Experiments
|
3
|
+
def self.pick_alternative(experiment)
|
4
|
+
experiments[experiment].sample
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.experiments
|
8
|
+
@experiments ||= YAML::load(File.open(experiments_path)).symbolize_keys
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.experiments_path
|
12
|
+
"#{Rails.root}/config/ab_experiments.yml"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/test/db/.keep
ADDED
File without changes
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require "fileutils"
|
2
|
+
require "#{File.dirname(__FILE__)}/test/test_helper"
|
3
|
+
|
4
|
+
module GuineaPig
|
5
|
+
module Experiments
|
6
|
+
def self.experiments_path
|
7
|
+
"#{File.dirname(__FILE__)}/test/fixtures/ab_experiments.yml"
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
user = User.create!
|
13
|
+
ab_test = GuineaPig.get(:experiment_monkey, user)
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require_relative "test_helper"
|
2
|
+
|
3
|
+
class GuineaPigTest < MiniTest::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
GuineaPig::ABTest.destroy_all
|
6
|
+
User.destroy_all
|
7
|
+
|
8
|
+
GuineaPig::Experiments.stubs(:experiments_path).returns("#{FIXTURES}/ab_experiments.yml")
|
9
|
+
@user = User.create!
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_get
|
13
|
+
ab_test = GuineaPig.get(:experiment_monkey, @user)
|
14
|
+
ab_test.reload
|
15
|
+
|
16
|
+
assert_equal("experiment_monkey", ab_test.experiment)
|
17
|
+
assert_includes(["alternative_monkey_1", "alternative_monkey_2"], ab_test.alternative)
|
18
|
+
assert_equal(@user, ab_test.guinea_pig)
|
19
|
+
assert_equal(0, ab_test.seen_count)
|
20
|
+
assert_equal(0, ab_test.conversion_count)
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_get_multiple_times_for_the_same_guinea_pig_should_return_the_same_alternative
|
24
|
+
first_ab_test =
|
25
|
+
assert_difference "GuineaPig::ABTest.count", 1 do
|
26
|
+
GuineaPig.get(:experiment_monkey, @user)
|
27
|
+
end
|
28
|
+
|
29
|
+
second_ab_test =
|
30
|
+
assert_difference "GuineaPig::ABTest.count", 0 do
|
31
|
+
GuineaPig.get(:experiment_monkey, @user)
|
32
|
+
end
|
33
|
+
|
34
|
+
assert_equal(first_ab_test, second_ab_test)
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_alternative
|
38
|
+
alternatives = 10.times.map { GuineaPig.alternative(:experiment_monkey, User.create!) }.uniq.sort
|
39
|
+
assert_equal(["alternative_monkey_1", "alternative_monkey_2"], alternatives)
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_update_seen_counter_in_alternative
|
43
|
+
GuineaPig.alternative(:experiment_monkey, @user)
|
44
|
+
assert_equal(1, GuineaPig.get(:experiment_monkey, @user).seen_count)
|
45
|
+
|
46
|
+
GuineaPig.alternative(:experiment_monkey, @user)
|
47
|
+
assert_equal(2, GuineaPig.get(:experiment_monkey, @user).seen_count)
|
48
|
+
end
|
49
|
+
|
50
|
+
def test_conversion
|
51
|
+
ab_test = GuineaPig::ABTest.create!(:experiment => :experiment_monkey, :guinea_pig => @user)
|
52
|
+
|
53
|
+
assert_equal(0, ab_test.conversion_count)
|
54
|
+
|
55
|
+
GuineaPig.conversion(:experiment_monkey, @user)
|
56
|
+
ab_test.reload
|
57
|
+
assert_equal(1, ab_test.conversion_count)
|
58
|
+
|
59
|
+
GuineaPig.conversion(:experiment_monkey, @user)
|
60
|
+
ab_test.reload
|
61
|
+
assert_equal(2, ab_test.conversion_count)
|
62
|
+
end
|
63
|
+
end
|
data/test/models.rb
ADDED
data/test/schema.rb
ADDED
data/test/test_helper.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require "minitest/unit"
|
2
|
+
require "minitest/autorun"
|
3
|
+
require "mocha/setup"
|
4
|
+
require "assert_difference"
|
5
|
+
require_relative "../lib/guinea_pig"
|
6
|
+
|
7
|
+
FileUtils.rm("#{File.dirname(__FILE__)}/db/guinea_pig.sqlite", :force => true)
|
8
|
+
|
9
|
+
ActiveRecord::Base.establish_connection(
|
10
|
+
:adapter => "sqlite3",
|
11
|
+
:database => "#{File.dirname(__FILE__)}/db/guinea_pig.sqlite"
|
12
|
+
)
|
13
|
+
|
14
|
+
require_relative "../lib/generators/guinea_pig/templates/create_ab_tests"
|
15
|
+
CreateAbTests.up
|
16
|
+
|
17
|
+
load("#{File.dirname(__FILE__)}/schema.rb")
|
18
|
+
load("#{File.dirname(__FILE__)}/models.rb")
|
19
|
+
|
20
|
+
class MiniTest::Unit::TestCase
|
21
|
+
include AssertDifference
|
22
|
+
FIXTURES = File.expand_path("#{File.dirname(__FILE__)}/fixtures")
|
23
|
+
end
|
24
|
+
|
metadata
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: guinea_pig
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.3
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Fernando Guillen
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-03-08 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rails
|
16
|
+
requirement: &70261810612440 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '3.0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70261810612440
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: mocha
|
27
|
+
requirement: &70261810611140 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70261810611140
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: sqlite3
|
38
|
+
requirement: &70261810609940 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70261810609940
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: assert_difference
|
49
|
+
requirement: &70261810609260 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *70261810609260
|
58
|
+
description: Very simple ABTest functionality for Ruby, based in ActiveRecord and
|
59
|
+
with Rails 3 generators
|
60
|
+
email:
|
61
|
+
- fguillen.mail@gmail.com
|
62
|
+
executables: []
|
63
|
+
extensions: []
|
64
|
+
extra_rdoc_files: []
|
65
|
+
files:
|
66
|
+
- .gitignore
|
67
|
+
- .rvmrc.example
|
68
|
+
- Gemfile
|
69
|
+
- LICENSE
|
70
|
+
- README.md
|
71
|
+
- Rakefile
|
72
|
+
- guinea_pig.gemspec
|
73
|
+
- lib/generators/guinea_pig/migration_generator.rb
|
74
|
+
- lib/generators/guinea_pig/templates/create_ab_tests.rb
|
75
|
+
- lib/guinea_pig.rb
|
76
|
+
- lib/guinea_pig/ab_test.rb
|
77
|
+
- lib/guinea_pig/experiments.rb
|
78
|
+
- lib/guinea_pig/version.rb
|
79
|
+
- test/db/.keep
|
80
|
+
- test/etc/test_session.rb
|
81
|
+
- test/fixtures/ab_experiments.yml
|
82
|
+
- test/guinea_pig_test.rb
|
83
|
+
- test/models.rb
|
84
|
+
- test/schema.rb
|
85
|
+
- test/test_helper.rb
|
86
|
+
homepage: https://github.com/fguillen/GuineaPig
|
87
|
+
licenses: []
|
88
|
+
post_install_message:
|
89
|
+
rdoc_options: []
|
90
|
+
require_paths:
|
91
|
+
- lib
|
92
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
93
|
+
none: false
|
94
|
+
requirements:
|
95
|
+
- - ! '>='
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '0'
|
98
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
99
|
+
none: false
|
100
|
+
requirements:
|
101
|
+
- - ! '>='
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
requirements: []
|
105
|
+
rubyforge_project:
|
106
|
+
rubygems_version: 1.8.15
|
107
|
+
signing_key:
|
108
|
+
specification_version: 3
|
109
|
+
summary: Very simple ABTest functionality for Ruby, based in ActiveRecord and with
|
110
|
+
Rails 3 generators
|
111
|
+
test_files:
|
112
|
+
- test/db/.keep
|
113
|
+
- test/etc/test_session.rb
|
114
|
+
- test/fixtures/ab_experiments.yml
|
115
|
+
- test/guinea_pig_test.rb
|
116
|
+
- test/models.rb
|
117
|
+
- test/schema.rb
|
118
|
+
- test/test_helper.rb
|