eeny-meeny 1.0.0 → 2.0.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 +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
|