field_test 0.1.1 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -0
- data/README.md +50 -11
- data/app/views/field_test/home/_experiments.html.erb +1 -1
- data/field_test.gemspec +1 -0
- data/lib/field_test.rb +9 -2
- data/lib/field_test/experiment.rb +20 -2
- data/lib/field_test/helpers.rb +12 -1
- data/lib/field_test/version.rb +1 -1
- data/lib/generators/field_test/templates/config.yml +3 -0
- metadata +16 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f8bbd74bb28099c186bacc0ea7b195c7d89a7e35
|
4
|
+
data.tar.gz: e87355a520385d34192ab200b80411a4bbc70310
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6f717922757f35cd62c835beb58bde3bce74fb326ca1965ddb5bbdd03811d1433fd99f1e677738b52d12f3c9158919faf832f99427d507ce5f23423b03757ccc
|
7
|
+
data.tar.gz: 5912035f52bfb111970e1fcf3f6bf73a9c4dbe55334e04b39a1ba9e456481ffe23f5838974989f45f560a16960b6f2a835bce2a0603c34991167fe39caa75230
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -16,7 +16,7 @@ Add this line to your application’s Gemfile:
|
|
16
16
|
gem 'field_test'
|
17
17
|
```
|
18
18
|
|
19
|
-
|
19
|
+
Run:
|
20
20
|
|
21
21
|
```sh
|
22
22
|
rails g field_test:install
|
@@ -30,6 +30,8 @@ mount FieldTest::Engine, at: "field_test"
|
|
30
30
|
|
31
31
|
Be sure to [secure the dashboard](#security) in production.
|
32
32
|
|
33
|
+
![Screenshot](https://ankane.github.io/field_test/screenshot5.png)
|
34
|
+
|
33
35
|
## Getting Started
|
34
36
|
|
35
37
|
Add an experiment to `config/field_test.yml`.
|
@@ -70,7 +72,7 @@ experiments:
|
|
70
72
|
winner: red
|
71
73
|
```
|
72
74
|
|
73
|
-
All calls to `field_test` will now return the winner.
|
75
|
+
All calls to `field_test` will now return the winner, and metrics will stop being recorded.
|
74
76
|
|
75
77
|
## Features
|
76
78
|
|
@@ -80,26 +82,67 @@ You can specify a variant with query parameters to make testing easier
|
|
80
82
|
http://localhost:3000/?field_test[button_color]=red
|
81
83
|
```
|
82
84
|
|
83
|
-
|
85
|
+
Assign a specific variant to a user with:
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
experiment = FieldTest::Experiment.find(:button_color)
|
89
|
+
experiment.variant(user, variant: "red")
|
90
|
+
```
|
91
|
+
|
92
|
+
Specify a participant with:
|
84
93
|
|
85
94
|
```ruby
|
86
95
|
field_test(:button_color, participant: "test@example.org")
|
87
96
|
```
|
88
97
|
|
98
|
+
You can pass an object as well.
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
field_test(:button_color, participant: user)
|
102
|
+
```
|
103
|
+
|
104
|
+
## Config
|
105
|
+
|
106
|
+
By default, bots are returned the first variant and excluded from metrics. Change this with:
|
107
|
+
|
108
|
+
```yml
|
109
|
+
exclude:
|
110
|
+
bots: false
|
111
|
+
```
|
112
|
+
|
89
113
|
Keep track of when experiments started and ended.
|
90
114
|
|
91
115
|
```yml
|
92
116
|
experiments:
|
93
|
-
|
117
|
+
button_color:
|
94
118
|
started_at: 2016-12-01 14:00:00
|
95
119
|
ended_at: 2016-12-08 14:00:00
|
96
120
|
```
|
97
121
|
|
122
|
+
By default, variants are given the same probability of being selected. Change this with:
|
123
|
+
|
124
|
+
```yml
|
125
|
+
experiments:
|
126
|
+
button_color:
|
127
|
+
variants:
|
128
|
+
- red
|
129
|
+
- blue
|
130
|
+
weights:
|
131
|
+
- 90
|
132
|
+
- 10
|
133
|
+
```
|
134
|
+
|
98
135
|
## Funnels
|
99
136
|
|
100
137
|
For advanced funnels, we recommend an analytics platform like [Ahoy](https://github.com/ankane/ahoy) or [Mixpanel](https://mixpanel.com/).
|
101
138
|
|
102
|
-
You can
|
139
|
+
You can use:
|
140
|
+
|
141
|
+
```ruby
|
142
|
+
field_test_experiments
|
143
|
+
```
|
144
|
+
|
145
|
+
to get all experiments and variants for a participant, and pass them as properties.
|
103
146
|
|
104
147
|
## Security
|
105
148
|
|
@@ -108,8 +151,8 @@ You can pass experiments and variants as properties.
|
|
108
151
|
Set the following variables in your environment or an initializer.
|
109
152
|
|
110
153
|
```ruby
|
111
|
-
ENV["FIELD_TEST_USERNAME"] = "
|
112
|
-
ENV["FIELD_TEST_PASSWORD"] = "
|
154
|
+
ENV["FIELD_TEST_USERNAME"] = "moonrise"
|
155
|
+
ENV["FIELD_TEST_PASSWORD"] = "kingdom"
|
113
156
|
```
|
114
157
|
|
115
158
|
#### Devise
|
@@ -124,10 +167,6 @@ end
|
|
124
167
|
|
125
168
|
A huge thanks to [Evan Miller](http://www.evanmiller.org/) for deriving the Bayesian formulas.
|
126
169
|
|
127
|
-
## TODO
|
128
|
-
|
129
|
-
- Exclude bots
|
130
|
-
|
131
170
|
## History
|
132
171
|
|
133
172
|
View the [changelog](https://github.com/ankane/field_test/blob/master/CHANGELOG.md)
|
data/field_test.gemspec
CHANGED
@@ -22,6 +22,7 @@ Gem::Specification.new do |spec|
|
|
22
22
|
spec.add_dependency "railties"
|
23
23
|
spec.add_dependency "activerecord"
|
24
24
|
spec.add_dependency "distribution"
|
25
|
+
spec.add_dependency "browser", "~> 2.0"
|
25
26
|
|
26
27
|
spec.add_development_dependency "bundler"
|
27
28
|
spec.add_development_dependency "rake"
|
data/lib/field_test.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require "distribution/math_extension"
|
2
|
+
require "browser"
|
2
3
|
require "field_test/experiment"
|
3
4
|
require "field_test/engine"
|
4
5
|
require "field_test/helpers"
|
@@ -7,14 +8,20 @@ require "field_test/version"
|
|
7
8
|
|
8
9
|
module FieldTest
|
9
10
|
class Error < StandardError; end
|
10
|
-
class ExperimentNotFound < Error; end
|
11
|
+
class ExperimentNotFound < Error; end
|
12
|
+
class UnknownParticipant < Error; end
|
11
13
|
|
12
|
-
def self.config
|
14
|
+
def self.config
|
13
15
|
# reload in dev
|
14
16
|
@config = nil if Rails.env.development?
|
15
17
|
|
16
18
|
@config ||= YAML.load(ERB.new(File.read("config/field_test.yml")).result)
|
17
19
|
end
|
20
|
+
|
21
|
+
def self.exclude_bots?
|
22
|
+
config = self.config # dev performance
|
23
|
+
config["exclude"] && config["exclude"]["bots"]
|
24
|
+
end
|
18
25
|
end
|
19
26
|
|
20
27
|
ActiveSupport.on_load(:action_controller) do
|
@@ -1,12 +1,13 @@
|
|
1
1
|
module FieldTest
|
2
2
|
class Experiment
|
3
|
-
attr_reader :id, :name, :variants, :winner, :started_at, :ended_at
|
3
|
+
attr_reader :id, :name, :variants, :weights, :winner, :started_at, :ended_at
|
4
4
|
|
5
5
|
def initialize(attributes)
|
6
6
|
attributes = attributes.symbolize_keys
|
7
7
|
@id = attributes[:id]
|
8
8
|
@name = attributes[:name] || @id.to_s.titleize
|
9
9
|
@variants = attributes[:variants]
|
10
|
+
@weights = @variants.size.times.map { |i| attributes[:weights].to_a[i] || 1 }
|
10
11
|
@winner = attributes[:winner]
|
11
12
|
@started_at = Time.zone.parse(attributes[:started_at].to_s) if attributes[:started_at]
|
12
13
|
@ended_at = Time.zone.parse(attributes[:ended_at].to_s) if attributes[:ended_at]
|
@@ -17,12 +18,13 @@ module FieldTest
|
|
17
18
|
return variants.first if options[:exclude]
|
18
19
|
|
19
20
|
participants = FieldTest::Participant.standardize(participants)
|
21
|
+
check_participants(participants)
|
20
22
|
membership = membership_for(participants) || FieldTest::Membership.new(experiment: id)
|
21
23
|
|
22
24
|
if options[:variant] && variants.include?(options[:variant])
|
23
25
|
membership.variant = options[:variant]
|
24
26
|
else
|
25
|
-
membership.variant ||=
|
27
|
+
membership.variant ||= weighted_variant
|
26
28
|
end
|
27
29
|
|
28
30
|
# upgrade to preferred participant
|
@@ -41,6 +43,7 @@ module FieldTest
|
|
41
43
|
|
42
44
|
def convert(participants)
|
43
45
|
participants = FieldTest::Participant.standardize(participants)
|
46
|
+
check_participants(participants)
|
44
47
|
membership = membership_for(participants)
|
45
48
|
|
46
49
|
if membership
|
@@ -117,11 +120,26 @@ module FieldTest
|
|
117
120
|
|
118
121
|
private
|
119
122
|
|
123
|
+
def check_participants(participants)
|
124
|
+
raise FieldTest::UnknownParticipant, "Use the :participant option to specify a participant" if participants.empty?
|
125
|
+
end
|
126
|
+
|
120
127
|
def membership_for(participants)
|
121
128
|
memberships = self.memberships.where(participant: participants).index_by(&:participant)
|
122
129
|
participants.map { |part| memberships[part] }.compact.first
|
123
130
|
end
|
124
131
|
|
132
|
+
def weighted_variant
|
133
|
+
total = weights.sum.to_f
|
134
|
+
pick = rand
|
135
|
+
n = 0
|
136
|
+
weights.map { |w| w / total }.each_with_index do |w, i|
|
137
|
+
n += w
|
138
|
+
return variants[i] if n >= pick
|
139
|
+
end
|
140
|
+
variants.last
|
141
|
+
end
|
142
|
+
|
125
143
|
# formula from
|
126
144
|
# http://www.evanmiller.org/bayesian-ab-testing.html
|
127
145
|
def prob_b_beats_a(alpha_a, beta_a, alpha_b, beta_b)
|
data/lib/field_test/helpers.rb
CHANGED
@@ -9,6 +9,10 @@ module FieldTest
|
|
9
9
|
if params[:field_test] && params[:field_test][experiment]
|
10
10
|
options[:variant] ||= params[:field_test][experiment]
|
11
11
|
end
|
12
|
+
|
13
|
+
if FieldTest.exclude_bots?
|
14
|
+
options[:exclude] = Browser.new(request.user_agent).bot?
|
15
|
+
end
|
12
16
|
end
|
13
17
|
|
14
18
|
# cache results for request
|
@@ -29,7 +33,7 @@ module FieldTest
|
|
29
33
|
memberships = FieldTest::Membership.where(participant: participants).group_by(&:participant)
|
30
34
|
experiments = {}
|
31
35
|
participants.each do |participant|
|
32
|
-
memberships[participant].each do |membership|
|
36
|
+
memberships[participant].to_a.each do |membership|
|
33
37
|
experiments[membership.experiment] ||= membership.variant
|
34
38
|
end
|
35
39
|
end
|
@@ -46,6 +50,7 @@ module FieldTest
|
|
46
50
|
participants << current_user
|
47
51
|
end
|
48
52
|
|
53
|
+
# controllers and views
|
49
54
|
if try(:request)
|
50
55
|
# use cookie
|
51
56
|
cookie_key = "field_test"
|
@@ -58,6 +63,12 @@ module FieldTest
|
|
58
63
|
participants << "cookie:#{token.gsub(/[^a-z0-9\-]/i, "")}"
|
59
64
|
end
|
60
65
|
end
|
66
|
+
|
67
|
+
# mailers
|
68
|
+
to = try(:message).try(:to).try(:first)
|
69
|
+
if to
|
70
|
+
participants << to
|
71
|
+
end
|
61
72
|
end
|
62
73
|
|
63
74
|
FieldTest::Participant.standardize(participants)
|
data/lib/field_test/version.rb
CHANGED
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.1.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Kane
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-12-
|
11
|
+
date: 2016-12-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: railties
|
@@ -52,6 +52,20 @@ dependencies:
|
|
52
52
|
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: browser
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '2.0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '2.0'
|
55
69
|
- !ruby/object:Gem::Dependency
|
56
70
|
name: bundler
|
57
71
|
requirement: !ruby/object:Gem::Requirement
|