easy_ab 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.editorconfig +8 -0
- data/.gitignore +2 -0
- data/CHANGELOG.md +0 -0
- data/Gemfile +2 -0
- data/LICENSE +21 -0
- data/README.md +100 -0
- data/app/models/easy_ab/grouping.rb +20 -0
- data/easy_ab.gemspec +16 -0
- data/lib/easy_ab/engine.rb +8 -0
- data/lib/easy_ab/experiment.rb +112 -0
- data/lib/easy_ab/helpers.rb +102 -0
- data/lib/easy_ab/participant.rb +7 -0
- data/lib/easy_ab/version.rb +3 -0
- data/lib/easy_ab.rb +56 -0
- data/lib/generators/easy_ab/install_generator.rb +36 -0
- data/lib/generators/easy_ab/templates/easy_ab.rb +26 -0
- data/lib/generators/easy_ab/templates/easy_ab.yml +8 -0
- data/lib/generators/easy_ab/templates/grouping.rb +15 -0
- metadata +62 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: bb6587e7ae47d2f3743e6dc6e6c053e40f993ac2
|
4
|
+
data.tar.gz: f74132d722d8dbb9f7dc275748770ecef6e406b1
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7c88e4fd12e3c20de12c8a00cae041ca279080501b0f57869b345f651b7b83cb695d21d02ad6b940ed5f9501993afa92e3eb52eb8dafd2fc52718ddb1ff45456
|
7
|
+
data.tar.gz: 2e0014482e2275c87bcabeb39cbc90f78012d6338ddf2075e4c22de991c90a7554580c7d4a8f052cc65457724355606c8951c88d7c717c693307cfda92a6184a
|
data/.editorconfig
ADDED
data/.gitignore
ADDED
data/CHANGELOG.md
ADDED
File without changes
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2017 Chien-Wei Chu
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
# Easy AB
|
2
|
+
|
3
|
+
Easy, flexible A/B testing tool for Rails.
|
4
|
+
|
5
|
+
* Design for web
|
6
|
+
* Use your database to keep users' testing info to seamlessly handle the transition from guest to signed in user. You don't need to prepare extra stack like Redis or something else.
|
7
|
+
* Grouping your users to your predefined variants with very easy and flexible way:
|
8
|
+
* Random with equal weightings.
|
9
|
+
* Random with predefined weightings.
|
10
|
+
* Define Proc(s) to setup your rules. Something like "sign up for 1 month to variant A, others to variant B", "user with odd id to variant A, with even id to variant B", ...
|
11
|
+
Example of using proc to define rules:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
# Example 1
|
15
|
+
variants: ['variant A', 'variant B']
|
16
|
+
rules: [
|
17
|
+
-> { current_user.created_at <= 1.month.ago },
|
18
|
+
-> { current_user.created_at > 1.month.ago },
|
19
|
+
]
|
20
|
+
|
21
|
+
# Example 2
|
22
|
+
variants: ['variant A', 'variant B']
|
23
|
+
rules: [
|
24
|
+
-> { current_user.id.odd? },
|
25
|
+
-> { current_user.id.even? },
|
26
|
+
]
|
27
|
+
```
|
28
|
+
and you can change your rules at any time without affecting existing users, they always see the same variant
|
29
|
+
* Convenient APIs to
|
30
|
+
* check your view for different variants
|
31
|
+
* output all experiments and the corresponding variants for your users. It's very useful when sending data to analytics services like Google Anayltics, Mixpanel, Kissmetrics, ...
|
32
|
+
* No DSL, just setup your rules with pure Ruby (and Rails) :)
|
33
|
+
|
34
|
+
# Notice
|
35
|
+
Easy AB is under development. Currently don't use in your production app.
|
36
|
+
|
37
|
+
# Why Easy AB?
|
38
|
+
## Comparisons
|
39
|
+
### Split
|
40
|
+
### Field Test
|
41
|
+
### Flipper
|
42
|
+
### ...
|
43
|
+
|
44
|
+
# Installation
|
45
|
+
|
46
|
+
* Add `gem 'easy_ab'` to your application's Gemfile and run `bundle install`.
|
47
|
+
* Run `bin/rails g easy_ab:install`. Migration file and initializer will copy to your app folder.
|
48
|
+
* Run `bin/rake db:migrate`
|
49
|
+
|
50
|
+
# Setup
|
51
|
+
|
52
|
+
Edit `config/initializers/easy_ab.rb` to setup basic configurations.
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
EasyAb.configure do |config|
|
56
|
+
# Tell the gem how to check whether current user is signed in or not
|
57
|
+
config.user_signed_in_method = -> { user_signed_in? }
|
58
|
+
|
59
|
+
# Tell the gem how to get current user's id
|
60
|
+
config.current_user_id = -> { current_user.id }
|
61
|
+
|
62
|
+
# Tell the gem how to check whether current user is admin or not
|
63
|
+
# Only admin can switch variant by url parameters
|
64
|
+
config.authorize_admin_with = -> { current_user.admin? }
|
65
|
+
end
|
66
|
+
```
|
67
|
+
|
68
|
+
# Getting Started
|
69
|
+
|
70
|
+
Setup your experiments in `config/initializers/easy_ab.rb`
|
71
|
+
|
72
|
+
Say, if you have an experiment named 'button color', with three equal weighted variants: red, blue, green.
|
73
|
+
|
74
|
+
Define your experiment as follows:
|
75
|
+
|
76
|
+
``` ruby
|
77
|
+
EasyAb.experiments do |experiment|
|
78
|
+
experiment.define :title_color, variants: ['red', 'blue', 'green']
|
79
|
+
end
|
80
|
+
```
|
81
|
+
|
82
|
+
Then you will be able to use the following helpers in controller or view:
|
83
|
+
|
84
|
+
```ruby
|
85
|
+
color = ab_test(:title_color)
|
86
|
+
```
|
87
|
+
|
88
|
+
or pass a block
|
89
|
+
|
90
|
+
```erb
|
91
|
+
<% ab_test(:title_color) do |color| %>
|
92
|
+
<h1 class="<%= color %>">Welcome!</h1>
|
93
|
+
<% end %>
|
94
|
+
```
|
95
|
+
|
96
|
+
For admin, specify a variant with url parameters makes debugging super easy:
|
97
|
+
|
98
|
+
```
|
99
|
+
?ab_test[title_color]=blue
|
100
|
+
```
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module EasyAb
|
2
|
+
class Grouping < ActiveRecord::Base
|
3
|
+
self.table_name = 'easy_ab_groupings'
|
4
|
+
|
5
|
+
validates :experiment, presence: true
|
6
|
+
validates :variant, presence: true
|
7
|
+
validates :user_id, uniqueness: { scope: [:experiment] }
|
8
|
+
validates :cookie, uniqueness: { scope: [:experiment] }
|
9
|
+
validate :user_should_be_present
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def user_should_be_present
|
14
|
+
if cookie.nil? && user_id.nil?
|
15
|
+
errors.add(:user_id, "or cookie can't be blank")
|
16
|
+
errors.add(:cookie, "or user_id can't be blank")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/easy_ab.gemspec
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
lib = File.expand_path('../lib', __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require 'easy_ab/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = 'easy_ab'
|
7
|
+
s.version = EasyAb::VERSION
|
8
|
+
s.summary = 'Flexible A/B testing and feature toggle for Rails'
|
9
|
+
s.authors = ['Gary Chu']
|
10
|
+
s.email = 'icarus4.chu@gmail.com'
|
11
|
+
s.files = `git ls-files -z`.split("\x0").reject do |f|
|
12
|
+
f.match(%r{^(test|spec|features)/})
|
13
|
+
end
|
14
|
+
s.homepage = 'https://github.com/icarus4/easy_ab'
|
15
|
+
s.license = 'MIT'
|
16
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
module EasyAb
|
2
|
+
class Experiment
|
3
|
+
attr_reader :name, :variants, :weights, :rules
|
4
|
+
|
5
|
+
def initialize(name, options = {})
|
6
|
+
@name = name.to_s
|
7
|
+
@variants = options[:variants]
|
8
|
+
@weights = options[:weights]
|
9
|
+
@rules = options[:rules]
|
10
|
+
|
11
|
+
raise ArgumentError, 'Please define variants' if @variants.blank?
|
12
|
+
raise ArgumentError, 'Number of variants and weights should be identical' if @weights.present? && @weights.size != @variants.size
|
13
|
+
raise ArgumentError, 'Number of variants and rules should be identical' if @rules.present? && @rules.size != @variants.size
|
14
|
+
raise ArgumentError, 'All rules should be a Proc' if @rules.present? && @rules.any? { |rule| !rule.is_a?(Proc) }
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.find_by_name!(experiment_name)
|
18
|
+
experiment_name = experiment_name.to_s
|
19
|
+
exp = EasyAb.experiments.all.find { |exp| exp.name == experiment_name }
|
20
|
+
raise ExperimentNotFound if exp.nil?
|
21
|
+
exp
|
22
|
+
end
|
23
|
+
|
24
|
+
def assign_variant(user_recognition, options = {})
|
25
|
+
grouping = find_grouping_by_user_recognition(user_recognition) || ::EasyAb::Grouping.new(experiment: name, user_id: user_recognition[:id], cookie: user_recognition[:cookie])
|
26
|
+
|
27
|
+
if options[:variant] && variants.include?(options[:variant])
|
28
|
+
grouping.variant = options[:variant]
|
29
|
+
else
|
30
|
+
grouping.variant ||= flexible_variant(options[:contexted_rules])
|
31
|
+
end
|
32
|
+
|
33
|
+
if grouping.changed? && !options[:skip_save]
|
34
|
+
begin
|
35
|
+
grouping.save!
|
36
|
+
rescue ActiveRecord::RecordNotUnique
|
37
|
+
grouping = find_grouping_by_user_recognition(user_recognition)
|
38
|
+
rescue ActiveRecord::RecordInvalid => e
|
39
|
+
if grouping.errors[:user_id].present? || grouping.errors[:cookie].present?
|
40
|
+
grouping = find_grouping_by_user_recognition(user_recognition)
|
41
|
+
else
|
42
|
+
raise e
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
grouping.variant
|
48
|
+
end
|
49
|
+
|
50
|
+
def find_grouping_by_user_recognition(user_recognition)
|
51
|
+
user_id = user_recognition[:id]
|
52
|
+
cookie = user_recognition[:cookie]
|
53
|
+
grouping = nil
|
54
|
+
|
55
|
+
raise 'should assign a cookie' unless cookie
|
56
|
+
|
57
|
+
if user_id # If user login
|
58
|
+
# Case I: User participated experiment with login and return again
|
59
|
+
# Case II: user participated experiment with login and return by another device with login
|
60
|
+
# Case III: user participated experiment with login and return by the same device, but cookie was cleared between last and this participation
|
61
|
+
# => Both II and III already exist a record with the same user_id but different cookie
|
62
|
+
# In the above two cases, we update the cookie of the exising record
|
63
|
+
return grouping if (grouping = groupings.where(user_id: user_id).first) && ((cookie && grouping.cookie = cookie) || true)
|
64
|
+
|
65
|
+
# User participated experiment without login, but this time with login => assign user_id to the existing record
|
66
|
+
return grouping if (grouping = groupings.where(user_id: nil, cookie: cookie).first) && grouping.user_id = user_id
|
67
|
+
else # If user not login
|
68
|
+
return grouping if grouping = groupings.where(cookie: cookie).first
|
69
|
+
end
|
70
|
+
|
71
|
+
# User have yet to participate experiment
|
72
|
+
nil
|
73
|
+
end
|
74
|
+
|
75
|
+
def groupings
|
76
|
+
::EasyAb::Grouping.where(experiment: name)
|
77
|
+
end
|
78
|
+
|
79
|
+
def flexible_variant(contexted_rules = nil)
|
80
|
+
if contexted_rules
|
81
|
+
variant_by_rule(contexted_rules)
|
82
|
+
elsif weights
|
83
|
+
weighted_variant
|
84
|
+
else
|
85
|
+
equal_weighted_variant
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def variant_by_rule(contexted_rules)
|
90
|
+
contexted_rules.each_with_index do |rule, i|
|
91
|
+
return variants[i] if rule.call
|
92
|
+
end
|
93
|
+
# If all rules not matched, apply the first variatn
|
94
|
+
variants.first
|
95
|
+
end
|
96
|
+
|
97
|
+
def weighted_variant
|
98
|
+
total = weights.sum
|
99
|
+
roll = rand
|
100
|
+
sum = 0
|
101
|
+
weights.each_with_index do |weight, i|
|
102
|
+
sum += weight.to_d / total
|
103
|
+
return variants[i] if sum >= roll
|
104
|
+
end
|
105
|
+
variants.last
|
106
|
+
end
|
107
|
+
|
108
|
+
def equal_weighted_variant
|
109
|
+
variants.sample
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module EasyAb
|
2
|
+
module Helpers
|
3
|
+
# Return variant of specified experiment for current user
|
4
|
+
def ab_test(experiment_name, options = {})
|
5
|
+
experiment_name = experiment_name.to_s
|
6
|
+
user_recognition = find_ab_test_user_recognition(options)
|
7
|
+
|
8
|
+
if respond_to?(:request) && params[:ab_test] && params[:ab_test][experiment_name]
|
9
|
+
# Check current user is admin or not by proc defined by gem user
|
10
|
+
if current_user_is_admin?
|
11
|
+
options[:variant] ||= params[:ab_test][experiment_name]
|
12
|
+
end
|
13
|
+
# TODO: exclude bot
|
14
|
+
end
|
15
|
+
|
16
|
+
experiment = EasyAb::Experiment.find_by_name!(experiment_name)
|
17
|
+
|
18
|
+
if experiment.rules.present?
|
19
|
+
@rules_with_current_context ||= experiment.rules.map { |rule| Proc.new { instance_exec(&rule)} }
|
20
|
+
options[:contexted_rules] = @rules_with_current_context
|
21
|
+
end
|
22
|
+
|
23
|
+
@variant_cache ||= {}
|
24
|
+
@variant_cache[experiment_name] ||= experiment.assign_variant(user_recognition, options)
|
25
|
+
block_given? ? yield(@variant_cache[experiment_name]) : @variant_cache[experiment_name]
|
26
|
+
end
|
27
|
+
|
28
|
+
# Return all participated experiments and the corresponding variants for current user
|
29
|
+
# Return format:
|
30
|
+
# {
|
31
|
+
# 'experiment 1' => 'variant 1',
|
32
|
+
# 'experiment 2' => 'variant 2',
|
33
|
+
# ...
|
34
|
+
# }
|
35
|
+
def participated_experiments(options = {})
|
36
|
+
user_recognition = find_ab_test_user_recognition(options)
|
37
|
+
groupings = if user_recognition[:id]
|
38
|
+
EasyAb::Grouping.where("user_id = ? OR cookie = ?", user_recognition[:id], user_recognition[:cookie])
|
39
|
+
else
|
40
|
+
EasyAb::Grouping.where(cookie: user_recognition[:cookie])
|
41
|
+
end
|
42
|
+
|
43
|
+
experiments = {}
|
44
|
+
groupings.each do |grouping|
|
45
|
+
experiments[grouping.experiment] = grouping.variant
|
46
|
+
end
|
47
|
+
experiments
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def find_ab_test_user_recognition(options = {})
|
53
|
+
user_recognition = {}
|
54
|
+
|
55
|
+
# TODO:
|
56
|
+
# return (raise NotImplementedError) if options[:user] && (users << options[:user])
|
57
|
+
|
58
|
+
user_recognition[:id] = current_user_id if current_user_signed_in?
|
59
|
+
# Controllers and views
|
60
|
+
user_recognition[:cookie] = find_or_create_easy_ab_cookie if respond_to?(:request)
|
61
|
+
|
62
|
+
user_recognition
|
63
|
+
end
|
64
|
+
|
65
|
+
def current_user_signed_in?
|
66
|
+
user_signed_in_method_proc.call
|
67
|
+
end
|
68
|
+
|
69
|
+
def current_user_id
|
70
|
+
current_user_id_proc.call
|
71
|
+
end
|
72
|
+
|
73
|
+
def current_user_is_admin?
|
74
|
+
authorize_admin_with_proc.call
|
75
|
+
end
|
76
|
+
|
77
|
+
def authorize_admin_with_proc
|
78
|
+
@authorize_admin_with_proc ||= Proc.new { instance_exec &EasyAb.config.authorize_admin_with }
|
79
|
+
end
|
80
|
+
|
81
|
+
def current_user_id_proc
|
82
|
+
@current_user_id_proc ||= Proc.new { instance_exec &EasyAb.config.current_user_id }
|
83
|
+
end
|
84
|
+
|
85
|
+
def user_signed_in_method_proc
|
86
|
+
@user_signed_in_method_proc ||= Proc.new { instance_exec &EasyAb.config.user_signed_in_method }
|
87
|
+
end
|
88
|
+
|
89
|
+
def find_or_create_easy_ab_cookie
|
90
|
+
cookie_key = :easy_ab
|
91
|
+
value = cookies[cookie_key]
|
92
|
+
value = if value
|
93
|
+
value.gsub(/[^a-z0-9\-]/i, "")
|
94
|
+
else
|
95
|
+
SecureRandom.uuid
|
96
|
+
end
|
97
|
+
cookies[cookie_key] = { value: value, expires: 30.days.from_now }
|
98
|
+
|
99
|
+
value
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
data/lib/easy_ab.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'easy_ab/experiment'
|
2
|
+
require 'easy_ab/engine' if defined?(Rails)
|
3
|
+
require 'easy_ab/helpers'
|
4
|
+
require 'easy_ab/version'
|
5
|
+
|
6
|
+
module EasyAb
|
7
|
+
class Error < StandardError; end
|
8
|
+
class ExperimentNotFound < Error; end
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def config
|
12
|
+
@@config
|
13
|
+
end
|
14
|
+
|
15
|
+
def configure
|
16
|
+
@@config ||= Config.new
|
17
|
+
yield(@@config)
|
18
|
+
end
|
19
|
+
|
20
|
+
def experiments
|
21
|
+
@@experiments ||= Experiments.new
|
22
|
+
|
23
|
+
if block_given?
|
24
|
+
yield(@@experiments)
|
25
|
+
else
|
26
|
+
@@experiments
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class Config
|
32
|
+
attr_accessor :authorize_admin_with, :user_signed_in_method, :current_user_id
|
33
|
+
end
|
34
|
+
|
35
|
+
class Experiments
|
36
|
+
def initialize
|
37
|
+
@experiments = []
|
38
|
+
end
|
39
|
+
|
40
|
+
def define(name, options = {})
|
41
|
+
@experiments << ::EasyAb::Experiment.new(name, options)
|
42
|
+
end
|
43
|
+
|
44
|
+
def all
|
45
|
+
@experiments
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
ActiveSupport.on_load(:action_controller) do
|
51
|
+
include ::EasyAb::Helpers
|
52
|
+
end
|
53
|
+
|
54
|
+
ActiveSupport.on_load(:action_view) do
|
55
|
+
include ::EasyAb::Helpers
|
56
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module EasyAb
|
2
|
+
module Generators
|
3
|
+
class InstallGenerator < ::Rails::Generators::Base
|
4
|
+
include Rails::Generators::Migration
|
5
|
+
source_root File.expand_path("../templates", __FILE__)
|
6
|
+
|
7
|
+
# Implement the required interface for Rails::Generators::Migration.
|
8
|
+
def self.next_migration_number(dirname)
|
9
|
+
next_migration_number = current_migration_number(dirname) + 1
|
10
|
+
if ::ActiveRecord::Base.timestamped_migrations
|
11
|
+
[Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % next_migration_number].max
|
12
|
+
else
|
13
|
+
"%.3d" % next_migration_number
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def migration_version
|
18
|
+
if ActiveRecord::VERSION::MAJOR >= 5
|
19
|
+
"[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def copy_migration
|
24
|
+
migration_template 'grouping.rb', 'db/migrate/create_easy_ab_groupings.rb', migration_version: migration_version
|
25
|
+
end
|
26
|
+
|
27
|
+
# def copy_config
|
28
|
+
# template 'easy_ab.yml', 'config/easy_ab.yml'
|
29
|
+
# end
|
30
|
+
|
31
|
+
def copy_initializer_file
|
32
|
+
copy_file 'easy_ab.rb', 'config/initializers/easy_ab.rb'
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
EasyAb.configure do |config|
|
2
|
+
# Define how to check whether current user is signed in or not
|
3
|
+
config.user_signed_in_method = -> { user_signed_in? }
|
4
|
+
|
5
|
+
# Define how to get current user's id
|
6
|
+
config.current_user_id = -> { current_user.id }
|
7
|
+
|
8
|
+
# Define how to check whether current user is admin or not
|
9
|
+
# Only admin can switch variant by url parameters
|
10
|
+
config.authorize_admin_with = -> { current_user.admin? }
|
11
|
+
end
|
12
|
+
|
13
|
+
EasyAb.experiments do |experiment|
|
14
|
+
experiment.define :button_color,
|
15
|
+
variants: ['red', 'blue', 'yellow'],
|
16
|
+
weights: [10, 3, 1]
|
17
|
+
|
18
|
+
experiment.define :title,
|
19
|
+
variants: ['hello', 'welcome', 'yo'],
|
20
|
+
rules: [
|
21
|
+
-> { (1..100).cover?(current_user.id) },
|
22
|
+
-> { current_user.id.odd? },
|
23
|
+
-> { current_user.id.even? },
|
24
|
+
]
|
25
|
+
|
26
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version %>
|
2
|
+
def change
|
3
|
+
create_table :easy_ab_groupings do |t|
|
4
|
+
t.integer :user_id
|
5
|
+
t.string :cookie
|
6
|
+
t.string :experiment
|
7
|
+
t.string :variant
|
8
|
+
t.timestamps
|
9
|
+
end
|
10
|
+
|
11
|
+
add_index :easy_ab_groupings, [:experiment, :user_id], unique: true
|
12
|
+
add_index :easy_ab_groupings, :user_id
|
13
|
+
add_index :easy_ab_groupings, :cookie
|
14
|
+
end
|
15
|
+
end
|
metadata
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: easy_ab
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Gary Chu
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-08-07 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description:
|
14
|
+
email: icarus4.chu@gmail.com
|
15
|
+
executables: []
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files: []
|
18
|
+
files:
|
19
|
+
- ".editorconfig"
|
20
|
+
- ".gitignore"
|
21
|
+
- CHANGELOG.md
|
22
|
+
- Gemfile
|
23
|
+
- LICENSE
|
24
|
+
- README.md
|
25
|
+
- app/models/easy_ab/grouping.rb
|
26
|
+
- easy_ab.gemspec
|
27
|
+
- lib/easy_ab.rb
|
28
|
+
- lib/easy_ab/engine.rb
|
29
|
+
- lib/easy_ab/experiment.rb
|
30
|
+
- lib/easy_ab/helpers.rb
|
31
|
+
- lib/easy_ab/participant.rb
|
32
|
+
- lib/easy_ab/version.rb
|
33
|
+
- lib/generators/easy_ab/install_generator.rb
|
34
|
+
- lib/generators/easy_ab/templates/easy_ab.rb
|
35
|
+
- lib/generators/easy_ab/templates/easy_ab.yml
|
36
|
+
- lib/generators/easy_ab/templates/grouping.rb
|
37
|
+
homepage: https://github.com/icarus4/easy_ab
|
38
|
+
licenses:
|
39
|
+
- MIT
|
40
|
+
metadata: {}
|
41
|
+
post_install_message:
|
42
|
+
rdoc_options: []
|
43
|
+
require_paths:
|
44
|
+
- lib
|
45
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '0'
|
50
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
requirements: []
|
56
|
+
rubyforge_project:
|
57
|
+
rubygems_version: 2.6.12
|
58
|
+
signing_key:
|
59
|
+
specification_version: 4
|
60
|
+
summary: Flexible A/B testing and feature toggle for Rails
|
61
|
+
test_files: []
|
62
|
+
has_rdoc:
|