vanity 2.2.6 → 2.2.7
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/.gitignore +2 -2
- data/CHANGELOG +9 -0
- data/Gemfile.lock +1 -1
- data/doc/ab_testing.textile +5 -0
- data/gemfiles/rails32.gemfile.lock +1 -1
- data/gemfiles/rails41.gemfile.lock +1 -1
- data/gemfiles/rails42.gemfile.lock +1 -1
- data/gemfiles/rails42_protected_attributes.gemfile.lock +1 -1
- data/gemfiles/rails5.gemfile.lock +1 -1
- data/lib/vanity/adapters/abstract_adapter.rb +10 -0
- data/lib/vanity/adapters/active_record_adapter.rb +6 -4
- data/lib/vanity/adapters/mock_adapter.rb +33 -9
- data/lib/vanity/adapters/mongodb_adapter.rb +5 -3
- data/lib/vanity/adapters/redis_adapter.rb +9 -7
- data/lib/vanity/connection.rb +49 -37
- data/lib/vanity/experiment/ab_test.rb +2 -2
- data/lib/vanity/frameworks/rails.rb +2 -2
- data/lib/vanity/version.rb +1 -1
- data/test/adapters/active_record_adapter_test.rb +19 -0
- data/test/adapters/mock_adapter_test.rb +16 -0
- data/test/adapters/mongodb_adapter_test.rb +19 -0
- data/test/adapters/redis_adapter_test.rb +12 -0
- data/test/adapters/shared_tests.rb +336 -0
- data/test/test_helper.rb +4 -2
- data/test/vanity_test.rb +9 -5
- metadata +11 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 30ab64490eb076e3145a45d198b65778073979d6
|
4
|
+
data.tar.gz: 318343bcd0ba889d85cef2c4a3e8cf7b95ac8d78
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 08ac7419f74bb5282753cf8f5303de34eea8b6bb91c9c9a2483d40d9b23475864940db3f64f27887409ff7bb98676a0c685e0847a6064f068c4402e366fe222a
|
7
|
+
data.tar.gz: 044f52924d4c36026360db12f36585517267ddeb3149a0858a8204c906966252144c6a9c4360aa6a03aba5aabf41ee0710e021bc026c927bac1a0b1045714b71
|
data/.gitignore
CHANGED
data/CHANGELOG
CHANGED
@@ -1,3 +1,12 @@
|
|
1
|
+
== Unreleased
|
2
|
+
|
3
|
+
== 2.2.7 (2016-12-21)
|
4
|
+
* Fix 'base 17' typo, correct id bucketing (#308, @bazzargh)
|
5
|
+
* Update docs about `rebalance_frequency` (#310, @sdhull)
|
6
|
+
* Resolve Rails 5 deprecation warning for Mime::HTML (#313, @terracatta)
|
7
|
+
* Update MockAdapter and testing using it (#316, #309 @urbanautomaton)
|
8
|
+
* Deprecate calling #ab_seen with alternative instance (#317, @urbanautomaton)
|
9
|
+
|
1
10
|
== 2.2.6 (2016-10-04)
|
2
11
|
* Fixes for `Vanity.ab_test()` vs. view helper `ab_test` (@phillbaker)
|
3
12
|
|
data/Gemfile.lock
CHANGED
data/doc/ab_testing.textile
CHANGED
@@ -162,9 +162,14 @@ ab_test "noodle_test" do
|
|
162
162
|
alternatives "spaghetti", "linguine"
|
163
163
|
metrics :signup
|
164
164
|
score_method :bayes_bandit_score
|
165
|
+
rebalance_frequency 100
|
165
166
|
end
|
166
167
|
</pre>
|
167
168
|
|
169
|
+
Note: Setting the score method to `bayes_bandit_score` won't adjust alternative probabilities (ie, you won't get the benefit of maximizing conversions) unless you also set `rebalance_frequency`, which controls how many impressions must pass before rebalancing alternative probabilities.
|
170
|
+
|
171
|
+
Also note that impressions count is stored in-memory and is therefore reset upon restart of your app.
|
172
|
+
|
168
173
|
h3(#test). A/B Testing and Code Testing
|
169
174
|
|
170
175
|
If you're presenting more than one alternative to visitors of your site, you'll want to test more than one alternative. Don't let A/B testing become A/broken.
|
@@ -142,6 +142,16 @@ module Vanity
|
|
142
142
|
fail "Not implemented"
|
143
143
|
end
|
144
144
|
|
145
|
+
private
|
146
|
+
|
147
|
+
def with_ab_seen_deprecation(experiment, identity, alternative)
|
148
|
+
if alternative.respond_to?(:id)
|
149
|
+
Vanity.configuration.logger.warn(%q{Deprecated: #ab_seen should be passed the alternative id, not an Alternative instance})
|
150
|
+
yield experiment, identity, alternative.id
|
151
|
+
else
|
152
|
+
yield experiment, identity, alternative
|
153
|
+
end
|
154
|
+
end
|
145
155
|
end
|
146
156
|
end
|
147
157
|
end
|
@@ -255,9 +255,11 @@ module Vanity
|
|
255
255
|
end
|
256
256
|
|
257
257
|
# Determines if a participant already has seen this alternative in this experiment.
|
258
|
-
def ab_seen(experiment, identity,
|
259
|
-
|
260
|
-
|
258
|
+
def ab_seen(experiment, identity, alternative_or_id)
|
259
|
+
with_ab_seen_deprecation(experiment, identity, alternative_or_id) do |expt, ident, alt_id|
|
260
|
+
participant = VanityParticipant.retrieve(expt, ident, false)
|
261
|
+
participant && participant.seen == alt_id
|
262
|
+
end
|
261
263
|
end
|
262
264
|
|
263
265
|
# Returns the participant's seen alternative in this experiment, if it exists
|
@@ -273,7 +275,7 @@ module Vanity
|
|
273
275
|
# previously recorded as participating in this experiment.
|
274
276
|
def ab_add_conversion(experiment, alternative, identity, count = 1, implicit = false)
|
275
277
|
participant = VanityParticipant.retrieve(experiment, identity, false)
|
276
|
-
VanityParticipant.retrieve(experiment, identity, implicit, :converted => alternative)
|
278
|
+
VanityParticipant.retrieve(experiment, identity, implicit, :converted => alternative, :seen => alternative)
|
277
279
|
VanityExperiment.retrieve(experiment).increment_conversion(alternative, count)
|
278
280
|
end
|
279
281
|
|
@@ -111,9 +111,7 @@ module Vanity
|
|
111
111
|
end
|
112
112
|
|
113
113
|
def ab_counts(experiment, alternative)
|
114
|
-
|
115
|
-
@experiments[experiment][:alternatives] ||= {}
|
116
|
-
alt = @experiments[experiment][:alternatives][alternative] ||= {}
|
114
|
+
alt = alternative(experiment, alternative)
|
117
115
|
{ :participants => alt[:participants] ? alt[:participants].size : 0,
|
118
116
|
:converted => alt[:converted] ? alt[:converted].size : 0,
|
119
117
|
:conversions => alt[:conversions] || 0 }
|
@@ -134,17 +132,29 @@ module Vanity
|
|
134
132
|
end
|
135
133
|
|
136
134
|
def ab_add_participant(experiment, alternative, identity)
|
137
|
-
|
138
|
-
@experiments[experiment][:alternatives] ||= {}
|
139
|
-
alt = @experiments[experiment][:alternatives][alternative] ||= {}
|
135
|
+
alt = alternative(experiment, alternative)
|
140
136
|
alt[:participants] ||= Set.new
|
141
137
|
alt[:participants] << identity
|
142
138
|
end
|
143
139
|
|
140
|
+
def ab_seen(experiment, identity, alternative_or_id)
|
141
|
+
with_ab_seen_deprecation(experiment, identity, alternative_or_id) do |expt, ident, alt_id|
|
142
|
+
if ab_assigned(expt, ident) == alt_id
|
143
|
+
alt_id
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def ab_assigned(experiment, identity)
|
149
|
+
alternatives_for(experiment).each do |alt_id, alt_state|
|
150
|
+
return alt_id if alt_state[:participants].include?(identity)
|
151
|
+
end
|
152
|
+
|
153
|
+
nil
|
154
|
+
end
|
155
|
+
|
144
156
|
def ab_add_conversion(experiment, alternative, identity, count = 1, implicit = false)
|
145
|
-
|
146
|
-
@experiments[experiment][:alternatives] ||= {}
|
147
|
-
alt = @experiments[experiment][:alternatives][alternative] ||= {}
|
157
|
+
alt = alternative(experiment, alternative)
|
148
158
|
alt[:participants] ||= Set.new
|
149
159
|
alt[:converted] ||= Set.new
|
150
160
|
alt[:conversions] ||= 0
|
@@ -170,6 +180,20 @@ module Vanity
|
|
170
180
|
def destroy_experiment(experiment)
|
171
181
|
@experiments.delete experiment
|
172
182
|
end
|
183
|
+
|
184
|
+
private
|
185
|
+
|
186
|
+
def alternative(experiment, alternative)
|
187
|
+
alternatives_for(experiment)[alternative] ||= {}
|
188
|
+
alternatives_for(experiment)[alternative]
|
189
|
+
end
|
190
|
+
|
191
|
+
def alternatives_for(experiment)
|
192
|
+
@experiments[experiment] ||= {}
|
193
|
+
@experiments[experiment][:alternatives] ||= {}
|
194
|
+
|
195
|
+
@experiments[experiment][:alternatives]
|
196
|
+
end
|
173
197
|
end
|
174
198
|
end
|
175
199
|
end
|
@@ -200,9 +200,11 @@ module Vanity
|
|
200
200
|
end
|
201
201
|
|
202
202
|
# Determines if a participant already has seen this alternative in this experiment.
|
203
|
-
def ab_seen(experiment, identity,
|
204
|
-
|
205
|
-
|
203
|
+
def ab_seen(experiment, identity, alternative_or_id)
|
204
|
+
with_ab_seen_deprecation(experiment, identity, alternative_or_id) do |expt, ident, alt_id|
|
205
|
+
participant = @participants.find(:experiment=>expt, :identity=>ident).limit(1).projection(:seen=>1).first
|
206
|
+
participant && participant["seen"].first == alt_id
|
207
|
+
end
|
206
208
|
end
|
207
209
|
|
208
210
|
# Returns the participant's seen alternative in this experiment, if it exists
|
@@ -87,7 +87,7 @@ module Vanity
|
|
87
87
|
|
88
88
|
def metric_values(metric, from, to)
|
89
89
|
single = @metrics.mget(*(from.to_date..to.to_date).map { |date| "#{metric}:#{date}:value:0" }) || []
|
90
|
-
single.map { |v| [v] }
|
90
|
+
single.map { |v| [v.to_i] }
|
91
91
|
end
|
92
92
|
|
93
93
|
def destroy_metric(metric)
|
@@ -175,12 +175,14 @@ module Vanity
|
|
175
175
|
end
|
176
176
|
end
|
177
177
|
|
178
|
-
def ab_seen(experiment, identity,
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
178
|
+
def ab_seen(experiment, identity, alternative_or_id)
|
179
|
+
with_ab_seen_deprecation(experiment, identity, alternative_or_id) do |expt, ident, alt_id|
|
180
|
+
call_redis_with_failover(expt, ident, alt_id) do
|
181
|
+
if @experiments.sismember "#{expt}:alts:#{alt_id}:participants", ident
|
182
|
+
alt_id
|
183
|
+
else
|
184
|
+
nil
|
185
|
+
end
|
184
186
|
end
|
185
187
|
end
|
186
188
|
end
|
data/lib/vanity/connection.rb
CHANGED
@@ -31,7 +31,7 @@ module Vanity
|
|
31
31
|
# )
|
32
32
|
# @since 2.0.0
|
33
33
|
def initialize(specification=nil)
|
34
|
-
@specification = specification || DEFAULT_SPECIFICATION
|
34
|
+
@specification = Specification.new(specification || DEFAULT_SPECIFICATION).to_h
|
35
35
|
|
36
36
|
if Autoconnect.playground_should_autoconnect?
|
37
37
|
@adapter = setup_connection(@specification)
|
@@ -54,47 +54,59 @@ module Vanity
|
|
54
54
|
|
55
55
|
private
|
56
56
|
|
57
|
-
def setup_connection(
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
57
|
+
def setup_connection(specification)
|
58
|
+
establish_connection(specification)
|
59
|
+
end
|
60
|
+
|
61
|
+
def establish_connection(spec)
|
62
|
+
Adapters.establish_connection(spec)
|
63
|
+
end
|
64
|
+
|
65
|
+
class Specification
|
66
|
+
def initialize(spec)
|
67
|
+
case spec
|
68
|
+
when String
|
69
|
+
@spec = build_specification_hash_from_url(spec)
|
70
|
+
when Hash
|
71
|
+
if spec[:redis]
|
72
|
+
@spec = {
|
73
|
+
adapter: :redis,
|
74
|
+
redis: spec[:redis]
|
75
|
+
}
|
76
|
+
else
|
77
|
+
@spec = spec
|
78
|
+
end
|
69
79
|
else
|
70
|
-
|
80
|
+
raise InvalidSpecification.new("Unsupported connection specification: #{spec.inspect}")
|
71
81
|
end
|
72
|
-
|
73
|
-
|
82
|
+
|
83
|
+
validate_specification_hash(@spec)
|
74
84
|
end
|
75
|
-
end
|
76
85
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
{
|
81
|
-
adapter: uri.scheme,
|
82
|
-
username: uri.user,
|
83
|
-
password: uri.password,
|
84
|
-
host: uri.host,
|
85
|
-
port: uri.port,
|
86
|
-
path: uri.path,
|
87
|
-
params: params
|
88
|
-
}
|
89
|
-
end
|
86
|
+
def to_h
|
87
|
+
@spec
|
88
|
+
end
|
90
89
|
|
91
|
-
|
92
|
-
all_symbol_keys = spec.keys.all? { |key| key.is_a?(Symbol) }
|
93
|
-
raise InvalidSpecification unless all_symbol_keys
|
94
|
-
end
|
90
|
+
private
|
95
91
|
|
96
|
-
|
97
|
-
|
92
|
+
def validate_specification_hash(spec)
|
93
|
+
all_symbol_keys = spec.keys.all? { |key| key.is_a?(Symbol) }
|
94
|
+
raise InvalidSpecification unless all_symbol_keys
|
95
|
+
end
|
96
|
+
|
97
|
+
def build_specification_hash_from_url(connection_url)
|
98
|
+
uri = URI.parse(connection_url)
|
99
|
+
params = CGI.parse(uri.query) if uri.query
|
100
|
+
{
|
101
|
+
adapter: uri.scheme,
|
102
|
+
username: uri.user,
|
103
|
+
password: uri.password,
|
104
|
+
host: uri.host,
|
105
|
+
port: uri.port,
|
106
|
+
path: uri.path,
|
107
|
+
params: params
|
108
|
+
}
|
109
|
+
end
|
98
110
|
end
|
99
111
|
end
|
100
|
-
end
|
112
|
+
end
|
@@ -610,7 +610,7 @@ module Vanity
|
|
610
610
|
end
|
611
611
|
end
|
612
612
|
|
613
|
-
Digest::MD5.hexdigest("#{name}/#{identity}").to_i(
|
613
|
+
Digest::MD5.hexdigest("#{name}/#{identity}").to_i(16) % @alternatives.size
|
614
614
|
end
|
615
615
|
|
616
616
|
# Saves the assignment of an alternative to a person and performs the
|
@@ -635,7 +635,7 @@ module Vanity
|
|
635
635
|
# if we have an on_assignment block, call it on new assignments
|
636
636
|
if defined?(@on_assignment_block) && @on_assignment_block
|
637
637
|
assignment = alternatives[index]
|
638
|
-
if !connection.ab_seen @id, identity,
|
638
|
+
if !connection.ab_seen @id, identity, index
|
639
639
|
@on_assignment_block.call(Vanity.context, identity, assignment, self)
|
640
640
|
end
|
641
641
|
end
|
@@ -348,7 +348,7 @@ module Vanity
|
|
348
348
|
# Step 3: Open your browser to http://localhost:3000/vanity
|
349
349
|
module Dashboard
|
350
350
|
def index
|
351
|
-
render :file=>Vanity.template("_report"),:content_type=>Mime
|
351
|
+
render :file=>Vanity.template("_report"),:content_type=>Mime[:html], :locals=>{
|
352
352
|
:experiments=>Vanity.playground.experiments,
|
353
353
|
:experiments_persisted=>Vanity.playground.experiments_persisted?,
|
354
354
|
:metrics=>Vanity.playground.metrics
|
@@ -356,7 +356,7 @@ module Vanity
|
|
356
356
|
end
|
357
357
|
|
358
358
|
def participant
|
359
|
-
render :file=>Vanity.template("_participant"), :locals=>{:participant_id => params[:id], :participant_info => Vanity.playground.participant_info(params[:id])}, :content_type=>Mime
|
359
|
+
render :file=>Vanity.template("_participant"), :locals=>{:participant_id => params[:id], :participant_info => Vanity.playground.participant_info(params[:id])}, :content_type=>Mime[:html]
|
360
360
|
end
|
361
361
|
|
362
362
|
def complete
|
data/lib/vanity/version.rb
CHANGED
@@ -0,0 +1,19 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
require 'vanity/adapters/active_record_adapter'
|
3
|
+
require 'adapters/shared_tests'
|
4
|
+
|
5
|
+
describe Vanity::Adapters::ActiveRecordAdapter do
|
6
|
+
|
7
|
+
def specification
|
8
|
+
Vanity::Connection::Specification.new(VanityTestHelpers::DATABASE_OPTIONS["active_record"])
|
9
|
+
end
|
10
|
+
|
11
|
+
def adapter
|
12
|
+
Vanity::Adapters::ActiveRecordAdapter.new(specification.to_h)
|
13
|
+
end
|
14
|
+
|
15
|
+
if ENV["DB"] == "active_record"
|
16
|
+
include Vanity::Adapters::SharedTests
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'adapters/shared_tests'
|
3
|
+
|
4
|
+
describe Vanity::Adapters::MockAdapter do
|
5
|
+
|
6
|
+
def specification
|
7
|
+
Vanity::Connection::Specification.new(VanityTestHelpers::DATABASE_OPTIONS["mock"])
|
8
|
+
end
|
9
|
+
|
10
|
+
def adapter
|
11
|
+
Vanity::Adapters::MockAdapter.new({})
|
12
|
+
end
|
13
|
+
|
14
|
+
include Vanity::Adapters::SharedTests
|
15
|
+
|
16
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
require 'vanity/adapters/mongodb_adapter'
|
3
|
+
require 'adapters/shared_tests'
|
4
|
+
|
5
|
+
describe Vanity::Adapters::MongodbAdapter do
|
6
|
+
|
7
|
+
def specification
|
8
|
+
Vanity::Connection::Specification.new(VanityTestHelpers::DATABASE_OPTIONS["mongodb"])
|
9
|
+
end
|
10
|
+
|
11
|
+
def adapter
|
12
|
+
Vanity::Adapters::MongodbAdapter.new(specification.to_h)
|
13
|
+
end
|
14
|
+
|
15
|
+
if ENV["DB"] == "mongodb"
|
16
|
+
include Vanity::Adapters::SharedTests
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
@@ -1,4 +1,6 @@
|
|
1
1
|
require "test_helper"
|
2
|
+
require 'vanity/adapters/redis_adapter'
|
3
|
+
require 'adapters/shared_tests'
|
2
4
|
|
3
5
|
describe Vanity::Adapters::RedisAdapter do
|
4
6
|
before do
|
@@ -6,6 +8,16 @@ describe Vanity::Adapters::RedisAdapter do
|
|
6
8
|
require "redis/namespace"
|
7
9
|
end
|
8
10
|
|
11
|
+
def specification
|
12
|
+
Vanity::Connection::Specification.new(VanityTestHelpers::DATABASE_OPTIONS["redis"])
|
13
|
+
end
|
14
|
+
|
15
|
+
def adapter
|
16
|
+
Vanity::Adapters::RedisAdapter.new(specification.to_h)
|
17
|
+
end
|
18
|
+
|
19
|
+
include Vanity::Adapters::SharedTests
|
20
|
+
|
9
21
|
it "warns on disconnect error" do
|
10
22
|
if defined?(Redis)
|
11
23
|
assert_silent do
|
@@ -0,0 +1,336 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
module Vanity::Adapters::SharedTests
|
4
|
+
|
5
|
+
DummyAlternative = Struct.new(:id)
|
6
|
+
|
7
|
+
def identity
|
8
|
+
'test-identity'
|
9
|
+
end
|
10
|
+
|
11
|
+
def experiment
|
12
|
+
:experiment
|
13
|
+
end
|
14
|
+
|
15
|
+
def alternative
|
16
|
+
1
|
17
|
+
end
|
18
|
+
|
19
|
+
def metric_name
|
20
|
+
:purchases
|
21
|
+
end
|
22
|
+
|
23
|
+
def run_experiment
|
24
|
+
@subject.ab_add_participant(experiment, alternative, identity)
|
25
|
+
@subject.ab_add_participant(experiment, alternative, 'other-identity')
|
26
|
+
@subject.ab_add_conversion(experiment, alternative, identity, 2)
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.included(base)
|
30
|
+
base.instance_eval do
|
31
|
+
before do
|
32
|
+
@subject = adapter
|
33
|
+
@subject.flushdb
|
34
|
+
|
35
|
+
metric "purchases"
|
36
|
+
|
37
|
+
new_ab_test "experiment" do
|
38
|
+
alternatives :control, :test
|
39
|
+
default :control
|
40
|
+
metrics :purchases
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe 'metrics methods' do
|
45
|
+
describe '#get_metric_last_update_at' do
|
46
|
+
it 'is nil if the metric has never been tracked' do
|
47
|
+
refute(@subject.get_metric_last_update_at(metric_name))
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'returns the time of the last tracked event if present' do
|
51
|
+
time = Time.now
|
52
|
+
@subject.metric_track(metric_name, time, identity, [1])
|
53
|
+
|
54
|
+
assert_in_delta(
|
55
|
+
time,
|
56
|
+
@subject.get_metric_last_update_at(metric_name),
|
57
|
+
1.0
|
58
|
+
)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
describe '#metric_values' do
|
63
|
+
it 'returns the tracked metrics from the given range, binned by date' do
|
64
|
+
time_1 = Time.iso8601("2016-01-01T00:00:00+00:00")
|
65
|
+
time_2 = Time.iso8601("2016-01-01T12:00:00+00:00")
|
66
|
+
time_3 = Time.iso8601("2016-01-02T00:00:00+00:00")
|
67
|
+
time_4 = Time.iso8601("2016-01-02T12:00:00+00:00")
|
68
|
+
time_5 = Time.iso8601("2016-01-03T12:00:00+00:00")
|
69
|
+
|
70
|
+
@subject.metric_track(metric_name, time_1, identity, [1])
|
71
|
+
@subject.metric_track(metric_name, time_2, identity, [2])
|
72
|
+
@subject.metric_track(metric_name, time_3, identity, [4])
|
73
|
+
@subject.metric_track(metric_name, time_4, identity, [8])
|
74
|
+
@subject.metric_track(metric_name, time_5, identity, [16])
|
75
|
+
|
76
|
+
assert_equal(
|
77
|
+
[[3], [12]],
|
78
|
+
@subject.metric_values(metric_name, time_1, time_4)
|
79
|
+
)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
describe '#destroy_metric' do
|
84
|
+
it 'removes all data related to the metric' do
|
85
|
+
@subject.metric_track(metric_name, Time.now, identity, [1])
|
86
|
+
|
87
|
+
@subject.destroy_metric(metric_name)
|
88
|
+
|
89
|
+
refute(@subject.get_metric_last_update_at(metric_name))
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
describe 'generic experiment methods' do
|
95
|
+
describe '#experiment_persisted?' do
|
96
|
+
it 'returns false if the experiment is unknown' do
|
97
|
+
refute(@subject.experiment_persisted?("other_experiment"))
|
98
|
+
end
|
99
|
+
|
100
|
+
it 'returns true if the experiment has been created' do
|
101
|
+
@subject.set_experiment_created_at("other_experiment", Time.now)
|
102
|
+
|
103
|
+
assert(@subject.experiment_persisted?("other_experiment"))
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
describe '#set_experiment_created_at' do
|
108
|
+
it 'sets the experiment creation date' do
|
109
|
+
time = Time.now
|
110
|
+
@subject.set_experiment_created_at(experiment, time)
|
111
|
+
|
112
|
+
assert_in_delta(
|
113
|
+
time,
|
114
|
+
@subject.get_experiment_created_at(experiment),
|
115
|
+
1.0
|
116
|
+
)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
describe '#destroy_experiment' do
|
121
|
+
it 'removes all information about the experiment' do
|
122
|
+
run_experiment
|
123
|
+
@subject.destroy_experiment(experiment)
|
124
|
+
|
125
|
+
refute(@subject.experiment_persisted?(experiment))
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
describe '#is_experiment_enabled?' do
|
130
|
+
def with_experiments_start_enabled(enabled)
|
131
|
+
begin
|
132
|
+
original_value = Vanity.configuration.experiments_start_enabled
|
133
|
+
Vanity.configuration.experiments_start_enabled = enabled
|
134
|
+
yield
|
135
|
+
ensure
|
136
|
+
Vanity.configuration.experiments_start_enabled = original_value
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
describe 'when experiments start enabled' do
|
141
|
+
it 'is true when the enabled value is unset' do
|
142
|
+
with_experiments_start_enabled(true) do
|
143
|
+
assert(@subject.is_experiment_enabled?("other_experiment"))
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
it 'is false when the enabled value is set to false' do
|
148
|
+
with_experiments_start_enabled(true) do
|
149
|
+
@subject.set_experiment_enabled("other_experiment", false)
|
150
|
+
|
151
|
+
refute(@subject.is_experiment_enabled?("other_experiment"))
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
describe 'when experiments do not start enabled' do
|
157
|
+
it 'is false when the enabled value is unset' do
|
158
|
+
with_experiments_start_enabled(false) do
|
159
|
+
refute(@subject.is_experiment_enabled?("other_experiment"))
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
it 'is true when the enabled value is set to true' do
|
164
|
+
with_experiments_start_enabled(false) do
|
165
|
+
@subject.set_experiment_enabled("other_experiment", true)
|
166
|
+
|
167
|
+
assert(@subject.is_experiment_enabled?("other_experiment"))
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
describe '#is_experiment_completed?' do
|
174
|
+
it 'is true if the completion date is set' do
|
175
|
+
@subject.set_experiment_completed_at(experiment, Time.now)
|
176
|
+
|
177
|
+
assert(@subject.is_experiment_completed?(experiment))
|
178
|
+
end
|
179
|
+
|
180
|
+
it 'is false if the completion date is unset' do
|
181
|
+
refute(@subject.is_experiment_completed?(experiment))
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
describe 'A/B test methods' do
|
187
|
+
describe '#ab_add_conversion' do
|
188
|
+
it 'adds the participant and the conversion when implicit=true' do
|
189
|
+
@subject.ab_add_conversion(experiment, alternative, identity, 1, true)
|
190
|
+
|
191
|
+
assert_equal(
|
192
|
+
{:participants => 1, :conversions => 1, :converted => 1},
|
193
|
+
@subject.ab_counts(experiment, alternative)
|
194
|
+
)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
describe '#ab_add_participant' do
|
199
|
+
it 'adds the participant to the specified alternative' do
|
200
|
+
@subject.ab_add_participant(experiment, alternative, identity)
|
201
|
+
|
202
|
+
assert_equal(
|
203
|
+
{:participants => 1, :conversions => 0, :converted => 0},
|
204
|
+
@subject.ab_counts(experiment, alternative)
|
205
|
+
)
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
describe '#ab_seen' do
|
210
|
+
describe 'called with an Alternative instance' do
|
211
|
+
def capture_logs
|
212
|
+
require 'stringio'
|
213
|
+
original_logger = Vanity.configuration.logger
|
214
|
+
log_output = StringIO.new
|
215
|
+
Vanity.configuration.logger = Logger.new(log_output)
|
216
|
+
yield
|
217
|
+
log_output.string
|
218
|
+
ensure
|
219
|
+
Vanity.configuration.logger = original_logger
|
220
|
+
end
|
221
|
+
|
222
|
+
before do
|
223
|
+
@alternative_instance = DummyAlternative.new(alternative)
|
224
|
+
end
|
225
|
+
|
226
|
+
it 'emits a deprecation warning' do
|
227
|
+
@subject.ab_add_participant(experiment, alternative, identity)
|
228
|
+
|
229
|
+
out = capture_logs do
|
230
|
+
@subject.ab_seen(experiment, identity, @alternative_instance)
|
231
|
+
end
|
232
|
+
|
233
|
+
assert_match(/Deprecated/, out)
|
234
|
+
end
|
235
|
+
|
236
|
+
it 'returns a truthy value if the identity is assigned to the alternative' do
|
237
|
+
@subject.ab_add_participant(experiment, alternative, identity)
|
238
|
+
|
239
|
+
assert(@subject.ab_seen(experiment, identity, @alternative_instance))
|
240
|
+
end
|
241
|
+
|
242
|
+
it 'returns a falsey value if the identity is not assigned to the alternative' do
|
243
|
+
@subject.ab_add_participant(experiment, alternative, identity)
|
244
|
+
|
245
|
+
refute(@subject.ab_seen(experiment, identity, DummyAlternative.new(2)))
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
describe 'called with an alternative id' do
|
250
|
+
it 'returns a truthy value if the identity is assigned to the alternative' do
|
251
|
+
@subject.ab_add_participant(experiment, alternative, identity)
|
252
|
+
|
253
|
+
assert(@subject.ab_seen(experiment, identity, alternative))
|
254
|
+
end
|
255
|
+
|
256
|
+
it 'returns a falsey value if the identity is not assigned to the alternative' do
|
257
|
+
@subject.ab_add_participant(experiment, alternative, identity)
|
258
|
+
|
259
|
+
refute(@subject.ab_seen(experiment, identity, 2))
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
describe '#ab_assigned' do
|
265
|
+
it 'returns the assigned alternative if present' do
|
266
|
+
@subject.ab_add_participant(experiment, alternative, identity)
|
267
|
+
|
268
|
+
assert_equal(
|
269
|
+
alternative,
|
270
|
+
@subject.ab_assigned(experiment, identity)
|
271
|
+
)
|
272
|
+
end
|
273
|
+
|
274
|
+
it 'returns nil if the identity has no assignment' do
|
275
|
+
assert_equal(
|
276
|
+
nil,
|
277
|
+
@subject.ab_assigned(experiment, identity)
|
278
|
+
)
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
describe '#ab_counts' do
|
283
|
+
it 'returns the counts of participants, conversions and converted for the alternative' do
|
284
|
+
run_experiment
|
285
|
+
|
286
|
+
assert_equal(
|
287
|
+
{:participants => 2, :conversions => 2, :converted => 1},
|
288
|
+
@subject.ab_counts(experiment, alternative)
|
289
|
+
)
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
describe '#ab_get_outcome' do
|
294
|
+
it 'returns the outcome if one is set' do
|
295
|
+
@subject.ab_set_outcome(experiment, alternative)
|
296
|
+
|
297
|
+
assert_equal(
|
298
|
+
alternative,
|
299
|
+
@subject.ab_get_outcome(experiment)
|
300
|
+
)
|
301
|
+
end
|
302
|
+
|
303
|
+
it 'returns nil otherwise' do
|
304
|
+
assert_equal(
|
305
|
+
nil,
|
306
|
+
@subject.ab_get_outcome(experiment)
|
307
|
+
)
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
describe '#ab_show' do
|
312
|
+
it 'forces an alternative to be shown to the given identity' do
|
313
|
+
@subject.ab_show(experiment, identity, alternative)
|
314
|
+
|
315
|
+
assert_equal(
|
316
|
+
alternative,
|
317
|
+
@subject.ab_showing(experiment, identity)
|
318
|
+
)
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
describe '#ab_not_showing' do
|
323
|
+
it 'cancels a previously-set showing alternative' do
|
324
|
+
@subject.ab_show(experiment, identity, alternative)
|
325
|
+
@subject.ab_not_showing(experiment, identity)
|
326
|
+
|
327
|
+
assert_equal(
|
328
|
+
nil,
|
329
|
+
@subject.ab_showing(experiment, identity)
|
330
|
+
)
|
331
|
+
end
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
335
|
+
end
|
336
|
+
end
|
data/test/test_helper.rb
CHANGED
@@ -43,12 +43,14 @@ module VanityTestHelpers
|
|
43
43
|
# We go destructive on the database at the end of each run, so make sure we
|
44
44
|
# don't use databases you care about. For Redis, we pick database 15
|
45
45
|
# (default is 0).
|
46
|
-
|
46
|
+
DATABASE_OPTIONS = {
|
47
47
|
"redis" => "redis://localhost/15",
|
48
48
|
"mongodb" => "mongodb://localhost/vanity",
|
49
49
|
"active_record" => { :adapter => "active_record", :active_record_adapter =>"default" },
|
50
50
|
"mock" => "mock:/"
|
51
|
-
}
|
51
|
+
}
|
52
|
+
|
53
|
+
DATABASE = DATABASE_OPTIONS[ENV["DB"]] or raise "No support yet for #{ENV["DB"]}"
|
52
54
|
|
53
55
|
TEST_DATA_FILES = Dir[File.expand_path("../data/*", __FILE__)]
|
54
56
|
VANITY_CONFIGS = TEST_DATA_FILES.each.with_object({}) do |path, hash|
|
data/test/vanity_test.rb
CHANGED
@@ -108,12 +108,16 @@ describe Vanity do
|
|
108
108
|
f.write(connection_config)
|
109
109
|
end
|
110
110
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
111
|
+
out, _err = capture_io do
|
112
|
+
Vanity.reset!
|
113
|
+
Vanity.configuration.logger = Logger.new($stdout)
|
114
|
+
Vanity.disconnect!
|
115
|
+
Vanity::Connection.stubs(:new)
|
116
|
+
Vanity.connect!
|
117
|
+
end
|
115
118
|
|
116
119
|
assert_equal false, Vanity.configuration.collecting
|
120
|
+
assert_match(/Deprecated/, out)
|
117
121
|
end
|
118
122
|
end
|
119
123
|
end
|
@@ -178,4 +182,4 @@ describe Vanity do
|
|
178
182
|
refute_same original_configuration, Vanity.reload!
|
179
183
|
end
|
180
184
|
end
|
181
|
-
end
|
185
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: vanity
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.2.
|
4
|
+
version: 2.2.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Assaf Arkin
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-12-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: i18n
|
@@ -224,7 +224,11 @@ files:
|
|
224
224
|
- lib/vanity/templates/vanity.js
|
225
225
|
- lib/vanity/vanity.rb
|
226
226
|
- lib/vanity/version.rb
|
227
|
+
- test/adapters/active_record_adapter_test.rb
|
228
|
+
- test/adapters/mock_adapter_test.rb
|
229
|
+
- test/adapters/mongodb_adapter_test.rb
|
227
230
|
- test/adapters/redis_adapter_test.rb
|
231
|
+
- test/adapters/shared_tests.rb
|
228
232
|
- test/autoconnect_test.rb
|
229
233
|
- test/cli_test.rb
|
230
234
|
- test/commands/report_test.rb
|
@@ -286,7 +290,7 @@ metadata: {}
|
|
286
290
|
post_install_message: To get started run vanity --help
|
287
291
|
rdoc_options:
|
288
292
|
- "--title"
|
289
|
-
- Vanity 2.2.
|
293
|
+
- Vanity 2.2.7
|
290
294
|
- "--main"
|
291
295
|
- README.md
|
292
296
|
- "--webcvs"
|
@@ -310,7 +314,11 @@ signing_key:
|
|
310
314
|
specification_version: 4
|
311
315
|
summary: Experience Driven Development framework for Ruby
|
312
316
|
test_files:
|
317
|
+
- test/adapters/active_record_adapter_test.rb
|
318
|
+
- test/adapters/mock_adapter_test.rb
|
319
|
+
- test/adapters/mongodb_adapter_test.rb
|
313
320
|
- test/adapters/redis_adapter_test.rb
|
321
|
+
- test/adapters/shared_tests.rb
|
314
322
|
- test/autoconnect_test.rb
|
315
323
|
- test/cli_test.rb
|
316
324
|
- test/commands/report_test.rb
|