field_test 0.1.1 → 0.1.2
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 +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
|
+

|
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
|