eeny-meeny 1.0.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -0
- data/README.md +175 -9
- data/eeny-meeny.gemspec +3 -2
- data/lib/eeny-meeny.rb +28 -1
- data/lib/eeny-meeny/experiment_helper.rb +12 -10
- data/lib/eeny-meeny/middleware.rb +14 -21
- data/lib/eeny-meeny/models/cookie.rb +104 -0
- data/lib/eeny-meeny/{encryptor.rb → models/encryptor.rb} +0 -0
- data/lib/eeny-meeny/{experiment.rb → models/experiment.rb} +19 -2
- data/lib/eeny-meeny/{variation.rb → models/variation.rb} +0 -0
- data/lib/eeny-meeny/railtie.rb +15 -14
- data/lib/eeny-meeny/routing/experiment_constraint.rb +19 -0
- data/lib/eeny-meeny/routing/smoke_test_constraint.rb +15 -0
- data/lib/eeny-meeny/version.rb +1 -1
- data/lib/tasks/cookie.rake +48 -0
- data/spec/eeny-meeny/middleware_spec.rb +15 -18
- data/spec/eeny-meeny/models/cookie_spec.rb +137 -0
- data/spec/eeny-meeny/models/experiment_spec.rb +181 -0
- data/spec/eeny-meeny/{variation_spec.rb → models/variation_spec.rb} +1 -1
- data/spec/eeny-meeny/routing/experiment_constraint_spec.rb +39 -0
- data/spec/eeny-meeny/routing/smoke_test_constraint_spec.rb +35 -0
- data/spec/fixtures/experiments.yml +12 -0
- data/spec/spec_helper.rb +18 -1
- data/spec/tasks/cookie_task_spec.rb +72 -0
- metadata +31 -13
- data/lib/eeny-meeny/middleware_helper.rb +0 -25
- data/lib/eeny-meeny/route_constraint.rb +0 -33
- data/lib/eeny-meeny/shared_methods.rb +0 -23
- data/spec/eeny-meeny/experiment_spec.rb +0 -62
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'eeny-meeny/routing/experiment_constraint'
|
2
|
+
require 'eeny-meeny/middleware'
|
3
|
+
require 'rack/test'
|
4
|
+
|
5
|
+
describe EenyMeeny::ExperimentConstraint, experiments: true do
|
6
|
+
|
7
|
+
let(:request) do
|
8
|
+
session = Rack::MockSession.new(EenyMeeny::Middleware.new(MockRackApp.new))
|
9
|
+
session.set_cookie('eeny_meeny_my_page_v1=IlI%2FGW9IZvayAGQbBOroxIrfr6Z116OJqdjFdrw6FOZXOrinmxQmsKw2a%2Fb8kJFP0Up%2BLr4FACovT9%2Bo0hRdcY0AJtcYqMXC96GDMSwa2HauZbjHw16Q3%2BboSnWjfaEOHmqlyxtPxQwxlr3rsT%2FYblPjqqQ%2FiPbaJUqou3LiMtpVg4V%2FJxJdhn0XJUgFMDaFWXVFYYA6VmJSFUGglhRlbg%3D%3D; path=/; expires=Tue, 11 Oct 2016 13:07:53 -0000; HttpOnly')
|
10
|
+
session
|
11
|
+
end
|
12
|
+
|
13
|
+
describe 'when initialized' do
|
14
|
+
|
15
|
+
context 'for an inactive experiment' do
|
16
|
+
subject do
|
17
|
+
described_class.new(:expired)
|
18
|
+
end
|
19
|
+
|
20
|
+
describe '#matches?' do
|
21
|
+
it 'returns false' do
|
22
|
+
expect(subject.matches?(request)).to be false
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
context 'for an active experiment' do
|
28
|
+
subject do
|
29
|
+
described_class.new(:my_page)
|
30
|
+
end
|
31
|
+
|
32
|
+
describe '#matches?' do
|
33
|
+
it 'returns true' do
|
34
|
+
expect(subject.matches?(request)).to be true
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'eeny-meeny/routing/smoke_test_constraint'
|
2
|
+
require 'eeny-meeny/middleware'
|
3
|
+
require 'rack/test'
|
4
|
+
|
5
|
+
describe EenyMeeny::SmokeTestConstraint do
|
6
|
+
|
7
|
+
let(:request) do
|
8
|
+
Rack::MockSession.new(EenyMeeny::Middleware.new(MockRackApp.new))
|
9
|
+
end
|
10
|
+
|
11
|
+
let(:request_with_cookie) do
|
12
|
+
request.set_cookie('smoke_test_shadow_v1=kqe%2Bt%2F72JZ9s7fOv0nQ8GszTEmmXj3tUsjqmqy31i4yZLku5okuya%2F3PYb8Oi%2BSi53hDP8egfeiCcbrlBN4s5ifQwToaZHNAs43V1EKb8ca%2BTRK0lpCWfR58%2BQjpWwZL; expires=Tue, 11 Oct 2016 13:30:31 -0000; HttpOnly')
|
13
|
+
request
|
14
|
+
end
|
15
|
+
|
16
|
+
describe 'when initialized' do
|
17
|
+
|
18
|
+
subject do
|
19
|
+
described_class.new(:shadow)
|
20
|
+
end
|
21
|
+
|
22
|
+
describe '#matches?' do
|
23
|
+
context 'for a request with a valid smoke test cookie' do
|
24
|
+
it 'returns true' do
|
25
|
+
expect(subject.matches?(request_with_cookie)).to be true
|
26
|
+
end
|
27
|
+
end
|
28
|
+
context 'for a request without a smoke test cookie' do
|
29
|
+
it 'returns false' do
|
30
|
+
expect(subject.matches?(request)).to be false
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -8,3 +8,15 @@
|
|
8
8
|
:new:
|
9
9
|
:name: New My Page
|
10
10
|
:weight: 0.98
|
11
|
+
:expired:
|
12
|
+
:name: Expired
|
13
|
+
:version: 1
|
14
|
+
:start_at: '2016-08-11T11:55:40Z'
|
15
|
+
:end_at: '2016-08-11T11:55:40Z'
|
16
|
+
:variations:
|
17
|
+
:old:
|
18
|
+
:name: Old Expired Page
|
19
|
+
:weight: 0.02
|
20
|
+
:new:
|
21
|
+
:name: New Expired Page
|
22
|
+
:weight: 0.98
|
data/spec/spec_helper.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'simplecov'
|
2
2
|
require 'simplecov-rcov'
|
3
3
|
require 'codeclimate-test-reporter'
|
4
|
+
require 'active_support/time'
|
4
5
|
|
5
6
|
SimpleCov.start do
|
6
7
|
formatter SimpleCov::Formatter::MultiFormatter[
|
@@ -9,16 +10,32 @@ SimpleCov.start do
|
|
9
10
|
CodeClimate::TestReporter::Formatter
|
10
11
|
]
|
11
12
|
add_group('EenyMeeny', 'lib/eeny-meeny')
|
13
|
+
add_group('Rake Tasks', 'lib/tasks')
|
12
14
|
add_group('Specs', 'spec')
|
13
15
|
end
|
14
16
|
|
15
17
|
require 'rspec'
|
16
18
|
require 'yaml'
|
17
|
-
require 'eeny-meeny'
|
18
19
|
require 'mock_rack_app'
|
19
20
|
|
21
|
+
require 'eeny-meeny'
|
22
|
+
|
20
23
|
RSpec.configure do |config|
|
21
24
|
config.run_all_when_everything_filtered = true
|
22
25
|
config.filter_run :focus
|
23
26
|
config.order = "random"
|
27
|
+
|
28
|
+
config.before(:suite) do
|
29
|
+
Time.zone = 'UTC'
|
30
|
+
end
|
31
|
+
|
32
|
+
config.before(:each) do
|
33
|
+
EenyMeeny.reset! # reset configuration before every test.
|
34
|
+
end
|
35
|
+
config.before(:each, experiments: true) do
|
36
|
+
EenyMeeny.configure do |config|
|
37
|
+
config.experiments = YAML.load_file(File.join('spec','fixtures','experiments.yml'))
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
24
41
|
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
describe 'cookie.rake', experiments: true do
|
5
|
+
before do
|
6
|
+
Rake.application.rake_require "tasks/cookie"
|
7
|
+
Rake::Task.define_task(:environment)
|
8
|
+
end
|
9
|
+
|
10
|
+
describe 'eeny_meeny:cookie:experiment' do
|
11
|
+
context 'executed with an experiment id' do
|
12
|
+
it 'generates a cookie' do
|
13
|
+
expect {
|
14
|
+
Rake::Task['eeny_meeny:cookie:experiment'].execute(Rake::TaskArguments.new([:experiment_id],['my_page']))
|
15
|
+
}.to_not raise_error
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
context 'executed without arguments' do
|
20
|
+
it 'results in an error' do
|
21
|
+
expect {
|
22
|
+
Rake::Task['eeny_meeny:cookie:experiment'].execute
|
23
|
+
}.to raise_error(RuntimeError, "Missing 'experiment_id' parameter")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe 'eeny_meeny:cookie:experiment_variation' do
|
29
|
+
context 'executed with an experiment id' do
|
30
|
+
it 'results in an error' do
|
31
|
+
expect {
|
32
|
+
Rake::Task['eeny_meeny:cookie:experiment_variation'].execute(Rake::TaskArguments.new([:experiment_id],['my_page']))
|
33
|
+
}.to raise_error(RuntimeError, "Missing 'variation_id' parameter")
|
34
|
+
end
|
35
|
+
|
36
|
+
context 'and a variation_id' do
|
37
|
+
it 'generates a cookie' do
|
38
|
+
expect {
|
39
|
+
Rake::Task['eeny_meeny:cookie:experiment_variation'].execute(Rake::TaskArguments.new([:experiment_id, :variation_id],['my_page', 'new']))
|
40
|
+
}.to_not raise_error
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
context 'executed without arguments' do
|
46
|
+
it 'results in an error' do
|
47
|
+
expect {
|
48
|
+
Rake::Task['eeny_meeny:cookie:experiment_variation'].execute
|
49
|
+
}.to raise_error(RuntimeError, "Missing 'experiment_id' parameter")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
describe 'eeny_meeny:cookie:smoke_test' do
|
55
|
+
context 'executed with an smoke test id' do
|
56
|
+
it 'generates a cookie' do
|
57
|
+
expect {
|
58
|
+
Rake::Task['eeny_meeny:cookie:smoke_test'].execute(Rake::TaskArguments.new([:smoke_test_id],['shadow']))
|
59
|
+
}.to_not raise_error
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
context 'executed without arguments' do
|
64
|
+
it 'results in an error' do
|
65
|
+
expect {
|
66
|
+
Rake::Task['eeny_meeny:cookie:smoke_test'].execute
|
67
|
+
}.to raise_error(RuntimeError, "Missing 'smoke_test_id' parameter")
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: eeny-meeny
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Christian Orthmann
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-09-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -80,6 +80,20 @@ dependencies:
|
|
80
80
|
- - "~>"
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rack-test
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 0.6.3
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 0.6.3
|
83
97
|
- !ruby/object:Gem::Dependency
|
84
98
|
name: rack
|
85
99
|
requirement: !ruby/object:Gem::Requirement
|
@@ -133,22 +147,27 @@ files:
|
|
133
147
|
- Rakefile
|
134
148
|
- eeny-meeny.gemspec
|
135
149
|
- lib/eeny-meeny.rb
|
136
|
-
- lib/eeny-meeny/encryptor.rb
|
137
|
-
- lib/eeny-meeny/experiment.rb
|
138
150
|
- lib/eeny-meeny/experiment_helper.rb
|
139
151
|
- lib/eeny-meeny/middleware.rb
|
140
|
-
- lib/eeny-meeny/
|
152
|
+
- lib/eeny-meeny/models/cookie.rb
|
153
|
+
- lib/eeny-meeny/models/encryptor.rb
|
154
|
+
- lib/eeny-meeny/models/experiment.rb
|
155
|
+
- lib/eeny-meeny/models/variation.rb
|
141
156
|
- lib/eeny-meeny/railtie.rb
|
142
|
-
- lib/eeny-meeny/
|
143
|
-
- lib/eeny-meeny/
|
144
|
-
- lib/eeny-meeny/variation.rb
|
157
|
+
- lib/eeny-meeny/routing/experiment_constraint.rb
|
158
|
+
- lib/eeny-meeny/routing/smoke_test_constraint.rb
|
145
159
|
- lib/eeny-meeny/version.rb
|
146
|
-
-
|
160
|
+
- lib/tasks/cookie.rake
|
147
161
|
- spec/eeny-meeny/middleware_spec.rb
|
148
|
-
- spec/eeny-meeny/
|
162
|
+
- spec/eeny-meeny/models/cookie_spec.rb
|
163
|
+
- spec/eeny-meeny/models/experiment_spec.rb
|
164
|
+
- spec/eeny-meeny/models/variation_spec.rb
|
165
|
+
- spec/eeny-meeny/routing/experiment_constraint_spec.rb
|
166
|
+
- spec/eeny-meeny/routing/smoke_test_constraint_spec.rb
|
149
167
|
- spec/fixtures/experiments.yml
|
150
168
|
- spec/mock_rack_app.rb
|
151
169
|
- spec/spec_helper.rb
|
170
|
+
- spec/tasks/cookie_task_spec.rb
|
152
171
|
homepage: http://rubygems.org/gems/eeny-meeny
|
153
172
|
licenses:
|
154
173
|
- MIT
|
@@ -169,9 +188,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
169
188
|
version: '0'
|
170
189
|
requirements: []
|
171
190
|
rubyforge_project:
|
172
|
-
rubygems_version: 2.
|
191
|
+
rubygems_version: 2.5.1
|
173
192
|
signing_key:
|
174
193
|
specification_version: 4
|
175
|
-
summary: A simple split testing tool for Rails
|
194
|
+
summary: A simple split and smoke testing tool for Rails
|
176
195
|
test_files: []
|
177
|
-
has_rdoc:
|
@@ -1,25 +0,0 @@
|
|
1
|
-
module EenyMeeny::MiddlewareHelper
|
2
|
-
def has_experiment_cookie?(cookies, experiment)
|
3
|
-
cookies.has_key?(experiment_cookie_name(experiment))
|
4
|
-
end
|
5
|
-
|
6
|
-
def generate_cookie_value(experiment, cookie_config)
|
7
|
-
variation = experiment.pick_variation
|
8
|
-
cookie = {
|
9
|
-
expires: (experiment.end_at || 1.year.from_now),
|
10
|
-
httponly: true,
|
11
|
-
value: Marshal.dump({
|
12
|
-
name: experiment.name,
|
13
|
-
variation: variation,
|
14
|
-
})
|
15
|
-
}
|
16
|
-
cookie[:same_site] = cookie_config[:same_site] unless cookie_config[:same_site].nil?
|
17
|
-
cookie[:path] = cookie_config[:path] unless cookie_config[:path].nil?
|
18
|
-
cookie
|
19
|
-
end
|
20
|
-
|
21
|
-
private
|
22
|
-
def experiment_cookie_name(experiment)
|
23
|
-
EenyMeeny::EENY_MEENY_COOKIE_PREFIX+experiment.id.to_s+'_v'+experiment.version.to_s
|
24
|
-
end
|
25
|
-
end
|
@@ -1,33 +0,0 @@
|
|
1
|
-
require 'eeny-meeny/shared_methods'
|
2
|
-
|
3
|
-
module EenyMeeny
|
4
|
-
class RouteConstraint
|
5
|
-
@@eeny_meeny_encryptor = nil
|
6
|
-
|
7
|
-
def initialize(experiment_id, variation_id: nil)
|
8
|
-
@experiment_id = experiment_id
|
9
|
-
@variation_id = variation_id
|
10
|
-
@version = experiment_version(experiment_id)
|
11
|
-
end
|
12
|
-
|
13
|
-
def matches?(request)
|
14
|
-
!participates_in?(request).nil?
|
15
|
-
end
|
16
|
-
|
17
|
-
private
|
18
|
-
|
19
|
-
def participates_in?(request)
|
20
|
-
cookie = eeny_meeny_cookie(request)
|
21
|
-
cookie[:variation] unless cookie.nil? || (!cookie.nil? && @variation_id.present? && @variation_id != cookie[:variation].id)
|
22
|
-
end
|
23
|
-
|
24
|
-
def eeny_meeny_cookie(request)
|
25
|
-
cookie = request.cookie_jar[EenyMeeny::EENY_MEENY_COOKIE_PREFIX+@experiment_id.to_s+'_v'+@version.to_s]
|
26
|
-
if cookie
|
27
|
-
Marshal.load(decrypt(cookie)) rescue nil
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
include EenyMeeny::SharedMethods
|
32
|
-
end
|
33
|
-
end
|
@@ -1,23 +0,0 @@
|
|
1
|
-
module EenyMeeny::SharedMethods
|
2
|
-
|
3
|
-
private
|
4
|
-
|
5
|
-
def experiment_version(experiment_id)
|
6
|
-
(Rails.application.config.eeny_meeny.experiments.
|
7
|
-
try(:[], experiment_id.to_sym).try(:[], :version) || 1) rescue 1
|
8
|
-
end
|
9
|
-
|
10
|
-
def decrypt(cookie)
|
11
|
-
begin
|
12
|
-
if Rails.application.config.eeny_meeny.secure
|
13
|
-
# Memoize encryptor to avoid creating new instances on every request.
|
14
|
-
@@eeny_meeny_encryptor ||= EenyMeeny::Encryptor.new(Rails.application.config.eeny_meeny.secret)
|
15
|
-
@@eeny_meeny_encryptor.decrypt(cookie)
|
16
|
-
else
|
17
|
-
cookie
|
18
|
-
end
|
19
|
-
rescue
|
20
|
-
nil
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
@@ -1,62 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
require 'eeny-meeny/experiment'
|
3
|
-
require 'eeny-meeny/variation'
|
4
|
-
|
5
|
-
describe EenyMeeny::Experiment do
|
6
|
-
describe 'when initialized' do
|
7
|
-
|
8
|
-
context 'with weighted variations' do
|
9
|
-
subject do
|
10
|
-
described_class.new(:experiment_1,
|
11
|
-
name: 'Test 1',
|
12
|
-
variations: {
|
13
|
-
a: { name: 'A', weight: 0.5 },
|
14
|
-
b: { name: 'B', weight: 0.3 }})
|
15
|
-
end
|
16
|
-
|
17
|
-
it 'sets the instance variables' do
|
18
|
-
expect(subject.id).to eq(:experiment_1)
|
19
|
-
expect(subject.name).to eq('Test 1')
|
20
|
-
expect(subject.variations).to be_a Array
|
21
|
-
expect(subject.variations.size).to eq(2)
|
22
|
-
end
|
23
|
-
|
24
|
-
it "has a 'total_weight' equal to the sum of the variation weights" do
|
25
|
-
expect(subject.total_weight).to eq(0.8)
|
26
|
-
end
|
27
|
-
|
28
|
-
describe '#pick_variation' do
|
29
|
-
it 'picks a variation' do
|
30
|
-
expect(subject.pick_variation).to be_a EenyMeeny::Variation
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
context 'with non-weighted variations' do
|
36
|
-
subject do
|
37
|
-
described_class.new(:experiment_1,
|
38
|
-
name: 'Test 1',
|
39
|
-
variations: {
|
40
|
-
a: { name: 'A' },
|
41
|
-
b: { name: 'B' }})
|
42
|
-
end
|
43
|
-
|
44
|
-
it 'sets the instance variables' do
|
45
|
-
expect(subject.id).to eq(:experiment_1)
|
46
|
-
expect(subject.name).to eq('Test 1')
|
47
|
-
expect(subject.variations).to be_a Array
|
48
|
-
expect(subject.variations.size).to eq(2)
|
49
|
-
end
|
50
|
-
|
51
|
-
it "has a 'total_weight' equal to the number of the variation weights" do
|
52
|
-
expect(subject.total_weight).to eq(2)
|
53
|
-
end
|
54
|
-
|
55
|
-
describe '#pick_variation' do
|
56
|
-
it 'picks a variation' do
|
57
|
-
expect(subject.pick_variation).to be_a EenyMeeny::Variation
|
58
|
-
end
|
59
|
-
end
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|