simple_split 0.1.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.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rubocop.yml +25 -0
- data/Gemfile +4 -0
- data/README.md +43 -0
- data/Rakefile +7 -0
- data/bin/bundler +16 -0
- data/bin/console +16 -0
- data/bin/rake +16 -0
- data/bin/setup +16 -0
- data/lib/simple_split/version.rb +3 -0
- data/lib/simple_split.rb +61 -0
- data/simple_split.gemspec +26 -0
- data/spec/lib/simple_split_spec.rb +34 -0
- data/spec/spec_helper.rb +8 -0
- metadata +133 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 10bbe495f89c5d80168e05747bbdf32cc0ed8911
|
4
|
+
data.tar.gz: 64f86c88ead59e49ad0b5fc46f0dd3fc22cc98b2
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: eec5fe8cf16d9d28f84e62a3c99436aafe17909b5e54ec3bdce9fc30b1272fdbdede0fa36048dbff718a58633255b510c3868282747759fc5b044d4d6bb845d4
|
7
|
+
data.tar.gz: 8bb9f98d5a22fa4bab376e41e96ebb5cc225543f35997735c12b665b792ebfa75ba084f2132ef4ce56acc1c8c4ec5fd0dfd8703aa2ddedb1eb22d95ddfd1c86b
|
data/.gitignore
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
Encoding:
|
2
|
+
Enabled: false
|
3
|
+
|
4
|
+
Documentation:
|
5
|
+
Enabled: false
|
6
|
+
|
7
|
+
ClassAndModuleChildren:
|
8
|
+
Enabled: false
|
9
|
+
|
10
|
+
ClassLength:
|
11
|
+
Enabled: false
|
12
|
+
|
13
|
+
MethodLength:
|
14
|
+
Enabled: false
|
15
|
+
|
16
|
+
DoubleNegation:
|
17
|
+
Enabled: false
|
18
|
+
|
19
|
+
AllCops:
|
20
|
+
Exclude:
|
21
|
+
- "bin/**/*"
|
22
|
+
- "db/**/*"
|
23
|
+
- "config/**/*"
|
24
|
+
- "Rakefile"
|
25
|
+
- "spec/i18n_spec.rb"
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
|
2
|
+
# simple_split – dead simple A/B testing on Rails
|
3
|
+
|
4
|
+
The `simple_split` gem provides the absolute simplest possible support for split
|
5
|
+
testing with Rails that could possibly exist. More feature-complete solutions
|
6
|
+
like [`split`][split] provide more bells and whistles, but they also do far
|
7
|
+
more than necessary for simple split testing with good analytics services.
|
8
|
+
|
9
|
+
Instead, `simple_split` is bare-bones while still supporting flexible testing.
|
10
|
+
|
11
|
+
- Supports weighted variations
|
12
|
+
- Does not require the use of any data store
|
13
|
+
- Variations already seen by users are tracked via cookies
|
14
|
+
|
15
|
+
## Quick Start
|
16
|
+
|
17
|
+
Add `gem 'simple_split'` to your Gemfile, then run `bundle`. That's it. Now you
|
18
|
+
can start adding split testing support to your project.
|
19
|
+
|
20
|
+
All classes that inherit from `ActionView` or `ActionController` have access to
|
21
|
+
the `ab_test` method. It accepts an experiment name and a list of variations.
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
ab_test 'experiment_name', 'variation_a', 'variation_b', 'variation_c'
|
25
|
+
```
|
26
|
+
|
27
|
+
The result of a call to `ab_test` is a randomly selected variation. If a user
|
28
|
+
has already seen a particular variation, that variation will always be returned
|
29
|
+
instead of randomly selecting one.
|
30
|
+
|
31
|
+
Weights can be specified using a hash.
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
ab_test 'experiment_name', { 'variation_a' => 1.0, 'variation_b' => 0.2 }
|
35
|
+
```
|
36
|
+
|
37
|
+
## Credit
|
38
|
+
|
39
|
+
This was inspired by the [`simple_abs`][simple_abs] gem, which was already quite
|
40
|
+
stripped-down, but it didn't quite suit my needs.
|
41
|
+
|
42
|
+
[split]: https://github.com/splitrb/split
|
43
|
+
[simple_abs]: https://github.com/n8/simple_abs
|
data/Rakefile
ADDED
data/bin/bundler
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# This file was generated by Bundler.
|
4
|
+
#
|
5
|
+
# The application 'bundler' is installed as part of a gem, and
|
6
|
+
# this file is here to facilitate running it.
|
7
|
+
#
|
8
|
+
|
9
|
+
require 'pathname'
|
10
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
|
11
|
+
Pathname.new(__FILE__).realpath)
|
12
|
+
|
13
|
+
require 'rubygems'
|
14
|
+
require 'bundler/setup'
|
15
|
+
|
16
|
+
load Gem.bin_path('bundler', 'bundler')
|
data/bin/console
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# This file was generated by Bundler.
|
4
|
+
#
|
5
|
+
# The application 'console' is installed as part of a gem, and
|
6
|
+
# this file is here to facilitate running it.
|
7
|
+
#
|
8
|
+
|
9
|
+
require 'pathname'
|
10
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
|
11
|
+
Pathname.new(__FILE__).realpath)
|
12
|
+
|
13
|
+
require 'rubygems'
|
14
|
+
require 'bundler/setup'
|
15
|
+
|
16
|
+
load Gem.bin_path('simple_split', 'console')
|
data/bin/rake
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# This file was generated by Bundler.
|
4
|
+
#
|
5
|
+
# The application 'rake' is installed as part of a gem, and
|
6
|
+
# this file is here to facilitate running it.
|
7
|
+
#
|
8
|
+
|
9
|
+
require 'pathname'
|
10
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
|
11
|
+
Pathname.new(__FILE__).realpath)
|
12
|
+
|
13
|
+
require 'rubygems'
|
14
|
+
require 'bundler/setup'
|
15
|
+
|
16
|
+
load Gem.bin_path('rake', 'rake')
|
data/bin/setup
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# This file was generated by Bundler.
|
4
|
+
#
|
5
|
+
# The application 'setup' is installed as part of a gem, and
|
6
|
+
# this file is here to facilitate running it.
|
7
|
+
#
|
8
|
+
|
9
|
+
require 'pathname'
|
10
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
|
11
|
+
Pathname.new(__FILE__).realpath)
|
12
|
+
|
13
|
+
require 'rubygems'
|
14
|
+
require 'bundler/setup'
|
15
|
+
|
16
|
+
load Gem.bin_path('simple_split', 'setup')
|
data/lib/simple_split.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'active_support/all'
|
2
|
+
require 'weighted_randomizer'
|
3
|
+
|
4
|
+
module SimpleSplit
|
5
|
+
class TestSelector
|
6
|
+
def initialize(get_test, set_test)
|
7
|
+
@get_test = get_test
|
8
|
+
@set_test = set_test
|
9
|
+
end
|
10
|
+
|
11
|
+
def get_test(name, tests)
|
12
|
+
get_existing_test_value(name, tests) || assign_test(name, tests)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def get_existing_test_value(name, tests)
|
18
|
+
test_value = @get_test.call name
|
19
|
+
test_value if test_value.present? && tests.include?(test_value)
|
20
|
+
end
|
21
|
+
|
22
|
+
def assign_test(name, tests)
|
23
|
+
selected_test = select_test tests
|
24
|
+
@set_test.call name, selected_test
|
25
|
+
selected_test
|
26
|
+
end
|
27
|
+
|
28
|
+
def select_test(tests)
|
29
|
+
WeightedRandomizer.new(normalize_tests tests).sample
|
30
|
+
end
|
31
|
+
|
32
|
+
def normalize_tests(tests)
|
33
|
+
tests.flat_map do |test|
|
34
|
+
if test.is_a? Hash
|
35
|
+
test
|
36
|
+
elsif test.is_a? Array
|
37
|
+
normalize_tests(test)
|
38
|
+
else
|
39
|
+
{ test => 1.0 }
|
40
|
+
end.to_a
|
41
|
+
end.to_h
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
module TestHelper
|
46
|
+
def ab_test(name, *tests)
|
47
|
+
TestSelector.new(->(k) { cookies[k] },
|
48
|
+
->(k, v) { cookies[k] = v })
|
49
|
+
.get_test name, tests
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
if defined? Rails
|
54
|
+
class Railtie < Rails::Railtie
|
55
|
+
initializer 'simple_split.initialize' do
|
56
|
+
ActionController::Base.send :include, SimpleSplit::TestHelper
|
57
|
+
ActionView::Base.send :include, SimpleSplit::TestHelper
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'simple_split/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'simple_split'
|
8
|
+
spec.version = SimpleSplit::VERSION
|
9
|
+
spec.authors = ['Alexis King']
|
10
|
+
spec.email = ['alexis@gophilosophie.com']
|
11
|
+
|
12
|
+
spec.summary = 'Dead simple, database-free AB testing in Rails'
|
13
|
+
spec.homepage = 'https://github.com/lexi-lambda/simple_split'
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0")
|
16
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
17
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
|
+
spec.require_paths = ['lib']
|
19
|
+
|
20
|
+
spec.add_development_dependency 'bundler', '~> 1.10'
|
21
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
22
|
+
spec.add_development_dependency 'minitest', '~> 5.7.0'
|
23
|
+
|
24
|
+
spec.add_runtime_dependency 'activesupport', '>= 3.0'
|
25
|
+
spec.add_runtime_dependency 'weighted_randomizer', '>= 0.1.2'
|
26
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe SimpleSplit::TestSelector do
|
4
|
+
let(:data_store) { Hash.new }
|
5
|
+
let(:selector) do
|
6
|
+
SimpleSplit::TestSelector.new(->(k) { data_store[k] },
|
7
|
+
->(k, v) { data_store[k] = v })
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'uses an existing test if one is already set' do
|
11
|
+
data_store[:example_experiment] = :test_a
|
12
|
+
_(selector.get_test :example_experiment, [:test_a, :test_b])
|
13
|
+
.must_equal :test_a
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'must save the chosen test if one does not yet exist' do
|
17
|
+
chosen_test = selector.get_test :example_experiment, [:test_a, :test_b]
|
18
|
+
_(data_store[:example_experiment]).must_equal chosen_test
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'must select tests proportionally to their weights' do
|
22
|
+
non_storing_selector =
|
23
|
+
SimpleSplit::TestSelector.new(->(_) { nil }, ->(_, _) {})
|
24
|
+
tests = [{ test_a: 1.0, test_b: 2.0 }]
|
25
|
+
|
26
|
+
number_of_trials = 1_000
|
27
|
+
test_results = number_of_trials.times.map do
|
28
|
+
non_storing_selector.get_test(:example_experiment, tests) == :test_a
|
29
|
+
end
|
30
|
+
test_a_frequency = test_results.count(true).to_f / number_of_trials
|
31
|
+
|
32
|
+
_(test_a_frequency).must_be_close_to 0.3, 0.1
|
33
|
+
end
|
34
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: simple_split
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Alexis King
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-07-13 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.10'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.10'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: minitest
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 5.7.0
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 5.7.0
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: activesupport
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: weighted_randomizer
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 0.1.2
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 0.1.2
|
83
|
+
description:
|
84
|
+
email:
|
85
|
+
- alexis@gophilosophie.com
|
86
|
+
executables:
|
87
|
+
- bundler
|
88
|
+
- console
|
89
|
+
- rake
|
90
|
+
- setup
|
91
|
+
extensions: []
|
92
|
+
extra_rdoc_files: []
|
93
|
+
files:
|
94
|
+
- ".gitignore"
|
95
|
+
- ".rubocop.yml"
|
96
|
+
- Gemfile
|
97
|
+
- README.md
|
98
|
+
- Rakefile
|
99
|
+
- bin/bundler
|
100
|
+
- bin/console
|
101
|
+
- bin/rake
|
102
|
+
- bin/setup
|
103
|
+
- lib/simple_split.rb
|
104
|
+
- lib/simple_split/version.rb
|
105
|
+
- simple_split.gemspec
|
106
|
+
- spec/lib/simple_split_spec.rb
|
107
|
+
- spec/spec_helper.rb
|
108
|
+
homepage: https://github.com/lexi-lambda/simple_split
|
109
|
+
licenses: []
|
110
|
+
metadata: {}
|
111
|
+
post_install_message:
|
112
|
+
rdoc_options: []
|
113
|
+
require_paths:
|
114
|
+
- lib
|
115
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
116
|
+
requirements:
|
117
|
+
- - ">="
|
118
|
+
- !ruby/object:Gem::Version
|
119
|
+
version: '0'
|
120
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
requirements: []
|
126
|
+
rubyforge_project:
|
127
|
+
rubygems_version: 2.4.5
|
128
|
+
signing_key:
|
129
|
+
specification_version: 4
|
130
|
+
summary: Dead simple, database-free AB testing in Rails
|
131
|
+
test_files:
|
132
|
+
- spec/lib/simple_split_spec.rb
|
133
|
+
- spec/spec_helper.rb
|