field_test 0.3.0 → 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of field_test might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/LICENSE.txt +1 -1
- data/README.md +20 -0
- data/app/views/field_test/experiments/_experiments.html.erb +1 -0
- data/app/views/layouts/field_test/application.html.erb +4 -0
- data/lib/field_test/controller.rb +22 -3
- data/lib/field_test/experiment.rb +25 -5
- data/lib/field_test/helpers.rb +9 -7
- data/lib/field_test/participant.rb +2 -2
- data/lib/field_test/version.rb +1 -1
- data/lib/generators/field_test/events_generator.rb +2 -15
- data/lib/generators/field_test/install_generator.rb +2 -15
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5935d4b9a0cca9bd7d08fb1e0acaf5ab6e0e7f8c8c44b71efae5a3667a0d37a6
|
4
|
+
data.tar.gz: e839006d04adfe1e06d27c2b32b8ea601ada47106b05537bf6a11168c8b0864a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0ab03821f1b5a95f8cf2681820ab4d13c10bde98889e2d5995cd05f7426f815d207bfcdd894db0da90acb1e1743adf6f7256c75d5f62572e27f7183a0992a722
|
7
|
+
data.tar.gz: 84009fba31741d3c737f0623b580512bed5dadd455c35691fbe0388899dcf706a8d5ed1740f3d94cb877058e19740aa5321a5b51ef7595c31cea483b85dfd1bc
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,14 @@
|
|
1
|
+
## 0.3.1 [unreleased]
|
2
|
+
|
3
|
+
- Added `closed` and `keep_variant`
|
4
|
+
- Added `field_test_upgrade_memberships` method
|
5
|
+
- Fixed API controller error
|
6
|
+
- Fixed bug where conversions were recorded after winner
|
7
|
+
|
8
|
+
Security
|
9
|
+
|
10
|
+
- Fixed arbitrary variants via query parameters - see [#17](https://github.com/ankane/field_test/issues/17)
|
11
|
+
|
1
12
|
## 0.3.0
|
2
13
|
|
3
14
|
- Added support for native apps
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
@@ -4,6 +4,7 @@
|
|
4
4
|
|
5
5
|
- Designed for web and email
|
6
6
|
- Comes with a [dashboard](https://fieldtest.dokkuapp.com/) to view results and update variants
|
7
|
+
- Uses your database for storage
|
7
8
|
- Seamlessly handles the transition from anonymous visitor to logged in user
|
8
9
|
|
9
10
|
Uses [Bayesian statistics](https://www.evanmiller.org/bayesian-ab-testing.html) to evaluate results so you don’t need to choose a sample size ahead of time.
|
@@ -76,6 +77,25 @@ experiments:
|
|
76
77
|
|
77
78
|
All calls to `field_test` will now return the winner, and metrics will stop being recorded.
|
78
79
|
|
80
|
+
You can keep returning the variant for existing participants after a winner is declared:
|
81
|
+
|
82
|
+
```yml
|
83
|
+
experiments:
|
84
|
+
button_color:
|
85
|
+
winner: green
|
86
|
+
keep_variant: true
|
87
|
+
```
|
88
|
+
|
89
|
+
You can also close an experiment to new participants without declaring a winner while still recording metrics for existing participants:
|
90
|
+
|
91
|
+
```yml
|
92
|
+
experiments:
|
93
|
+
button_color:
|
94
|
+
closed: true
|
95
|
+
```
|
96
|
+
|
97
|
+
Calls to `field_test` for new participants will return the control, and they won’t be added to the experiment.
|
98
|
+
|
79
99
|
You can get the list of experiments and variants for a user with:
|
80
100
|
|
81
101
|
```ruby
|
@@ -11,6 +11,20 @@ module FieldTest
|
|
11
11
|
end
|
12
12
|
end
|
13
13
|
|
14
|
+
def field_test_upgrade_memberships(options = {})
|
15
|
+
participants = FieldTest::Participant.standardize(options[:participant] || field_test_participant)
|
16
|
+
preferred = participants.first
|
17
|
+
Array(participants[1..-1]).each do |participant|
|
18
|
+
# can do this in single query once legacy_participants is removed
|
19
|
+
FieldTest::Membership.where(participant.where_values).each do |membership|
|
20
|
+
membership.participant = preferred.participant if membership.respond_to?(:participant=)
|
21
|
+
membership.participant_type = preferred.type if membership.respond_to?(:participant_type=)
|
22
|
+
membership.participant_id = preferred.id if membership.respond_to?(:participant_id=)
|
23
|
+
membership.save!
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
14
28
|
private
|
15
29
|
|
16
30
|
def field_test_participant
|
@@ -23,22 +37,27 @@ module FieldTest
|
|
23
37
|
|
24
38
|
cookie_key = "field_test"
|
25
39
|
|
40
|
+
# name not entirely accurate
|
41
|
+
# can still set cookies in ActionController::API through request.cookie_jar
|
42
|
+
# however, best to prompt developer to pass participant manually
|
43
|
+
cookies_supported = respond_to?(:cookies, true)
|
44
|
+
|
26
45
|
if request.headers["Field-Test-Visitor"]
|
27
46
|
token = request.headers["Field-Test-Visitor"]
|
28
|
-
elsif FieldTest.cookies
|
47
|
+
elsif FieldTest.cookies && cookies_supported
|
29
48
|
token = cookies[cookie_key]
|
30
49
|
|
31
50
|
if participants.empty? && !token
|
32
51
|
token = SecureRandom.uuid
|
33
52
|
cookies[cookie_key] = {value: token, expires: 30.days.from_now}
|
34
53
|
end
|
35
|
-
|
54
|
+
elsif !FieldTest.cookies
|
36
55
|
# anonymity set
|
37
56
|
# note: hashing does not conceal input
|
38
57
|
token = Digest::UUID.uuid_v5(FieldTest::UUID_NAMESPACE, ["visitor", FieldTest.mask_ip(request.remote_ip), request.user_agent].join("/"))
|
39
58
|
|
40
59
|
# delete cookie if present
|
41
|
-
cookies.delete(cookie_key) if cookies[cookie_key]
|
60
|
+
cookies.delete(cookie_key) if cookies_supported && cookies[cookie_key]
|
42
61
|
end
|
43
62
|
|
44
63
|
# sanitize tokens
|
@@ -10,6 +10,8 @@ module FieldTest
|
|
10
10
|
@variants = attributes[:variants]
|
11
11
|
@weights = @variants.size.times.map { |i| attributes[:weights].to_a[i] || 1 }
|
12
12
|
@winner = attributes[:winner]
|
13
|
+
@closed = attributes[:closed]
|
14
|
+
@keep_variant = attributes[:keep_variant]
|
13
15
|
@started_at = Time.zone.parse(attributes[:started_at].to_s) if attributes[:started_at]
|
14
16
|
@ended_at = Time.zone.parse(attributes[:ended_at].to_s) if attributes[:ended_at]
|
15
17
|
@goals = attributes[:goals] || ["conversion"]
|
@@ -18,17 +20,21 @@ module FieldTest
|
|
18
20
|
end
|
19
21
|
|
20
22
|
def variant(participants, options = {})
|
21
|
-
return winner if winner
|
22
|
-
return
|
23
|
+
return winner if winner && !keep_variant?
|
24
|
+
return control if options[:exclude]
|
23
25
|
|
24
26
|
participants = FieldTest::Participant.standardize(participants)
|
25
27
|
check_participants(participants)
|
26
28
|
membership = membership_for(participants) || FieldTest::Membership.new(experiment: id)
|
27
29
|
|
30
|
+
if winner # and keep_variant?
|
31
|
+
return membership.variant || winner
|
32
|
+
end
|
33
|
+
|
28
34
|
if options[:variant] && variants.include?(options[:variant])
|
29
35
|
membership.variant = options[:variant]
|
30
36
|
else
|
31
|
-
membership.variant ||= weighted_variant
|
37
|
+
membership.variant ||= closed? ? control : weighted_variant
|
32
38
|
end
|
33
39
|
|
34
40
|
participant = participants.first
|
@@ -38,7 +44,7 @@ module FieldTest
|
|
38
44
|
membership.participant_type = participant.type if membership.respond_to?(:participant_type=)
|
39
45
|
membership.participant_id = participant.id if membership.respond_to?(:participant_id=)
|
40
46
|
|
41
|
-
if membership.changed?
|
47
|
+
if membership.changed? && (!closed? || membership.persisted?)
|
42
48
|
begin
|
43
49
|
membership.save!
|
44
50
|
rescue ActiveRecord::RecordNotUnique
|
@@ -46,10 +52,12 @@ module FieldTest
|
|
46
52
|
end
|
47
53
|
end
|
48
54
|
|
49
|
-
membership.try(:variant) ||
|
55
|
+
membership.try(:variant) || control
|
50
56
|
end
|
51
57
|
|
52
58
|
def convert(participants, goal: nil)
|
59
|
+
return false if winner
|
60
|
+
|
53
61
|
goal ||= goals.first
|
54
62
|
|
55
63
|
participants = FieldTest::Participant.standardize(participants)
|
@@ -174,6 +182,18 @@ module FieldTest
|
|
174
182
|
!winner
|
175
183
|
end
|
176
184
|
|
185
|
+
def closed?
|
186
|
+
@closed
|
187
|
+
end
|
188
|
+
|
189
|
+
def keep_variant?
|
190
|
+
@keep_variant
|
191
|
+
end
|
192
|
+
|
193
|
+
def control
|
194
|
+
variants.first
|
195
|
+
end
|
196
|
+
|
177
197
|
def use_events?
|
178
198
|
if @use_events.nil?
|
179
199
|
FieldTest.events_supported?
|
data/lib/field_test/helpers.rb
CHANGED
@@ -1,12 +1,14 @@
|
|
1
1
|
module FieldTest
|
2
2
|
module Helpers
|
3
|
-
def field_test(experiment, options
|
3
|
+
def field_test(experiment, **options)
|
4
4
|
exp = FieldTest::Experiment.find(experiment)
|
5
5
|
|
6
|
-
participants = FieldTest::Participant.standardize(field_test_participant
|
6
|
+
participants = FieldTest::Participant.standardize(options[:participant] || field_test_participant)
|
7
7
|
|
8
8
|
if try(:request)
|
9
|
-
|
9
|
+
options = options.dup
|
10
|
+
|
11
|
+
if !options[:variant] && params[:field_test] && params[:field_test][experiment] && exp.variants.include?(params[:field_test][experiment])
|
10
12
|
params_variant = params[:field_test][experiment]
|
11
13
|
end
|
12
14
|
|
@@ -25,17 +27,17 @@ module FieldTest
|
|
25
27
|
@field_test_cache[experiment] ||= params_variant || exp.variant(participants, options)
|
26
28
|
end
|
27
29
|
|
28
|
-
def field_test_converted(experiment, options
|
30
|
+
def field_test_converted(experiment, **options)
|
29
31
|
exp = FieldTest::Experiment.find(experiment)
|
30
32
|
|
31
|
-
participants = FieldTest::Participant.standardize(field_test_participant
|
33
|
+
participants = FieldTest::Participant.standardize(options[:participant] || field_test_participant)
|
32
34
|
|
33
35
|
exp.convert(participants, goal: options[:goal])
|
34
36
|
end
|
35
37
|
|
36
38
|
# TODO fetch in single query
|
37
|
-
def field_test_experiments(options
|
38
|
-
participants = FieldTest::Participant.standardize(field_test_participant
|
39
|
+
def field_test_experiments(**options)
|
40
|
+
participants = FieldTest::Participant.standardize(options[:participant] || field_test_participant)
|
39
41
|
experiments = {}
|
40
42
|
participants.each do |participant|
|
41
43
|
FieldTest::Membership.where(participant.where_values).each do |membership|
|
@@ -31,8 +31,8 @@ module FieldTest
|
|
31
31
|
end
|
32
32
|
end
|
33
33
|
|
34
|
-
def self.standardize(
|
35
|
-
Array(
|
34
|
+
def self.standardize(participant)
|
35
|
+
Array(participant).compact.map { |v| FieldTest::Participant.new(v) }
|
36
36
|
end
|
37
37
|
end
|
38
38
|
end
|
data/lib/field_test/version.rb
CHANGED
@@ -1,23 +1,10 @@
|
|
1
|
-
require "rails/generators"
|
2
|
-
require "rails/generators/migration"
|
3
|
-
require "active_record"
|
4
1
|
require "rails/generators/active_record"
|
5
2
|
|
6
3
|
module FieldTest
|
7
4
|
module Generators
|
8
5
|
class EventsGenerator < Rails::Generators::Base
|
9
|
-
include
|
10
|
-
source_root File.
|
11
|
-
|
12
|
-
# Implement the required interface for Rails::Generators::Migration.
|
13
|
-
def self.next_migration_number(dirname) #:nodoc:
|
14
|
-
next_migration_number = current_migration_number(dirname) + 1
|
15
|
-
if ::ActiveRecord::Base.timestamped_migrations
|
16
|
-
[Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % next_migration_number].max
|
17
|
-
else
|
18
|
-
"%.3d" % next_migration_number
|
19
|
-
end
|
20
|
-
end
|
6
|
+
include ActiveRecord::Generators::Migration
|
7
|
+
source_root File.join(__dir__, "templates")
|
21
8
|
|
22
9
|
def copy_migration
|
23
10
|
migration_template "events.rb", "db/migrate/create_field_test_events.rb", migration_version: migration_version
|
@@ -1,23 +1,10 @@
|
|
1
|
-
require "rails/generators"
|
2
|
-
require "rails/generators/migration"
|
3
|
-
require "active_record"
|
4
1
|
require "rails/generators/active_record"
|
5
2
|
|
6
3
|
module FieldTest
|
7
4
|
module Generators
|
8
5
|
class InstallGenerator < Rails::Generators::Base
|
9
|
-
include
|
10
|
-
source_root File.
|
11
|
-
|
12
|
-
# Implement the required interface for Rails::Generators::Migration.
|
13
|
-
def self.next_migration_number(dirname) #:nodoc:
|
14
|
-
next_migration_number = current_migration_number(dirname) + 1
|
15
|
-
if ::ActiveRecord::Base.timestamped_migrations
|
16
|
-
[Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % next_migration_number].max
|
17
|
-
else
|
18
|
-
"%.3d" % next_migration_number
|
19
|
-
end
|
20
|
-
end
|
6
|
+
include ActiveRecord::Generators::Migration
|
7
|
+
source_root File.join(__dir__, "templates")
|
21
8
|
|
22
9
|
def copy_migration
|
23
10
|
migration_template "memberships.rb", "db/migrate/create_field_test_memberships.rb", migration_version: migration_version
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: field_test
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Kane
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-07-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: railties
|
@@ -205,7 +205,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
205
205
|
- !ruby/object:Gem::Version
|
206
206
|
version: '0'
|
207
207
|
requirements: []
|
208
|
-
rubygems_version: 3.0.
|
208
|
+
rubygems_version: 3.0.4
|
209
209
|
signing_key:
|
210
210
|
specification_version: 4
|
211
211
|
summary: A/B testing for Rails
|