field_test 0.3.2 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2a60826c451cf3807f5e111f2b4a619f4c07db0011ff27e7f2ceb0dd03bd1807
4
- data.tar.gz: 1150b7a05035bf91193dcdaca20a7075b1350d50ef037afebe6a1866baea5fb9
3
+ metadata.gz: 0b7676acb9075e7701de0019e3c7ca18a0242db8c87348d024006ff787c42d13
4
+ data.tar.gz: 92849469d6ca65422ef3f2a1b77f08c7695d79fa336b463cedf3991770be15cd
5
5
  SHA512:
6
- metadata.gz: 9a2644d12172b876b33ddc7faec2c4490623fed35aa7ae1848f0486a9ff176a80c2bdc2db66ec9196b93034f8566dbfc11c2027f524177a946427b6c4b10d71b
7
- data.tar.gz: 1dc282f0f054e7a95bdc4dfccd1361c11f28b53d70a2b5083ba73e40d3af13230246ad85e38990f74e7dab01a48fb5be9fefc5b2e6e1d8dff8cf066bb1dc525a
6
+ metadata.gz: 9f21fa35a6f44d085bbbfbd15e8174e7dd7f8a0d411e3a584097d59793776a0c0bd783cb4a3d7fee7f07402190a1a61487e1be554492208536f375b3e1981c7f
7
+ data.tar.gz: aeb943baab224022931a634fa190bbb3837856b3f0ee956c919f1233c6c6796a5251216a65334cd34878487e5902a251e695722a8982a19ffca6711e1d6113cf
data/CHANGELOG.md CHANGED
@@ -1,3 +1,21 @@
1
+ ## 0.5.1 (2021-09-22)
2
+
3
+ - Improved performance of Bayesian calculations
4
+
5
+ ## 0.5.0 (2021-09-21)
6
+
7
+ - Significantly improved performance of Bayesian calculations
8
+ - Dropped support for Ruby < 2.6 and Rails < 5.2
9
+
10
+ ## 0.4.1 (2020-09-07)
11
+
12
+ - Use `datetime` type in migration
13
+
14
+ ## 0.4.0 (2020-08-04)
15
+
16
+ - Fixed CSRF vulnerability with non-session based authentication - [more info](https://github.com/ankane/field_test/issues/28)
17
+ - Fixed cache key for requests
18
+
1
19
  ## 0.3.2 (2020-04-16)
2
20
 
3
21
  - Added support for excluding IP addresses
@@ -11,7 +29,7 @@
11
29
 
12
30
  Security
13
31
 
14
- - Fixed arbitrary variants via query parameters - see [#17](https://github.com/ankane/field_test/issues/17)
32
+ - Fixed arbitrary variants via query parameters - [more info](https://github.com/ankane/field_test/issues/17)
15
33
 
16
34
  ## 0.3.0 (2019-06-02)
17
35
 
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2016-2019 Andrew Kane
1
+ Copyright (c) 2016-2021 Andrew Kane
2
2
 
3
3
  MIT License
4
4
 
data/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
 
10
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.
11
11
 
12
- [![Build Status](https://travis-ci.org/ankane/field_test.svg?branch=master)](https://travis-ci.org/ankane/field_test)
12
+ [![Build Status](https://github.com/ankane/field_test/workflows/build/badge.svg?branch=master)](https://github.com/ankane/field_test/actions)
13
13
 
14
14
  ## Installation
15
15
 
@@ -160,7 +160,7 @@ experiment = FieldTest::Experiment.find(:button_color)
160
160
  button_color = experiment.variant(user)
161
161
  ```
162
162
 
163
- ## Config
163
+ ## Exclusions
164
164
 
165
165
  By default, bots are returned the first variant and excluded from metrics. Change this with:
166
166
 
@@ -178,6 +178,14 @@ exclude:
178
178
  - 10.0.0.0/8
179
179
  ```
180
180
 
181
+ You can also use custom logic:
182
+
183
+ ```ruby
184
+ field_test(:button_color, exclude: request.user_agent == "Test")
185
+ ```
186
+
187
+ ## Config
188
+
181
189
  Keep track of when experiments started and ended. Use any format `Time.parse` accepts. Variants assigned outside this window are not included in metrics.
182
190
 
183
191
  ```yml
@@ -2,7 +2,7 @@ module FieldTest
2
2
  class BaseController < ActionController::Base
3
3
  layout "field_test/application"
4
4
 
5
- protect_from_forgery
5
+ protect_from_forgery with: :exception
6
6
 
7
7
  http_basic_authenticate_with name: ENV["FIELD_TEST_USERNAME"], password: ENV["FIELD_TEST_PASSWORD"] if ENV["FIELD_TEST_PASSWORD"]
8
8
  end
@@ -0,0 +1,103 @@
1
+ #pragma once
2
+
3
+ #include <cmath>
4
+ #include <vector>
5
+
6
+ namespace bayesian_ab {
7
+
8
+ double logbeta(double a, double b) {
9
+ return std::lgamma(a) + std::lgamma(b) - std::lgamma(a + b);
10
+ }
11
+
12
+ double prob_b_beats_a(int alpha_a, int beta_a, int alpha_b, int beta_b) {
13
+ double total = 0.0;
14
+ double logbeta_aa_ba = logbeta(alpha_a, beta_a);
15
+ double beta_ba = beta_b + beta_a;
16
+
17
+ for (auto i = 0; i < alpha_b; i++) {
18
+ total += exp(logbeta(alpha_a + i, beta_ba) - log(beta_b + i) - logbeta(1 + i, beta_b) - logbeta_aa_ba);
19
+ }
20
+
21
+ return total;
22
+ }
23
+
24
+ double prob_c_beats_a_and_b(int alpha_a, int beta_a, int alpha_b, int beta_b, int alpha_c, int beta_c) {
25
+ double total = 0.0;
26
+
27
+ double logbeta_ac_bc = logbeta(alpha_c, beta_c);
28
+
29
+ std::vector<double> log_bb_j_logbeta_j_bb;
30
+ log_bb_j_logbeta_j_bb.reserve(alpha_b);
31
+
32
+ for (auto j = 0; j < alpha_b; j++) {
33
+ log_bb_j_logbeta_j_bb.push_back(log(beta_b + j) + logbeta(1 + j, beta_b));
34
+ }
35
+
36
+ double abc = beta_a + beta_b + beta_c;
37
+ std::vector<double> logbeta_ac_i_j;
38
+ logbeta_ac_i_j.reserve(alpha_a + alpha_b);
39
+
40
+ for (auto i = 0; i < alpha_a + alpha_b; i++) {
41
+ logbeta_ac_i_j.push_back(logbeta(alpha_c + i, abc));
42
+ }
43
+
44
+ for (auto i = 0; i < alpha_a; i++) {
45
+ double sum_i = -log(beta_a + i) - logbeta(1 + i, beta_a) - logbeta_ac_bc;
46
+
47
+ for (auto j = 0; j < alpha_b; j++) {
48
+ total += exp(sum_i + logbeta_ac_i_j[i + j] - log_bb_j_logbeta_j_bb[j]);
49
+ }
50
+ }
51
+
52
+ return 1 - prob_b_beats_a(alpha_c, beta_c, alpha_a, beta_a) -
53
+ prob_b_beats_a(alpha_c, beta_c, alpha_b, beta_b) + total;
54
+ }
55
+
56
+ double prob_d_beats_a_and_b_and_c(int alpha_a, int beta_a, int alpha_b, int beta_b, int alpha_c, int beta_c, int alpha_d, int beta_d) {
57
+ double total = 0.0;
58
+
59
+ double logbeta_ad_bd = logbeta(alpha_d, beta_d);
60
+
61
+ std::vector<double> log_bb_j_logbeta_j_bb;
62
+ log_bb_j_logbeta_j_bb.reserve(alpha_b);
63
+
64
+ for (auto j = 0; j < alpha_b; j++) {
65
+ log_bb_j_logbeta_j_bb.push_back(log(beta_b + j) + logbeta(1 + j, beta_b));
66
+ }
67
+
68
+ std::vector<double> log_bc_k_logbeta_k_bc;
69
+ log_bc_k_logbeta_k_bc.reserve(alpha_c);
70
+
71
+ for (auto k = 0; k < alpha_c; k++) {
72
+ log_bc_k_logbeta_k_bc.push_back(log(beta_c + k) + logbeta(1 + k, beta_c));
73
+ }
74
+
75
+ double abcd = beta_a + beta_b + beta_c + beta_d;
76
+ std::vector<double> logbeta_bd_i_j_k;
77
+ logbeta_bd_i_j_k.reserve(alpha_a + alpha_b + alpha_c);
78
+
79
+ for (auto i = 0; i < alpha_a + alpha_b + alpha_c; i++) {
80
+ logbeta_bd_i_j_k.push_back(logbeta(alpha_d + i, abcd));
81
+ }
82
+
83
+ for (auto i = 0; i < alpha_a; i++) {
84
+ double sum_i = -log(beta_a + i) - logbeta(1 + i, beta_a) - logbeta_ad_bd;
85
+
86
+ for (auto j = 0; j < alpha_b; j++) {
87
+ double sum_j = sum_i - log_bb_j_logbeta_j_bb[j];
88
+
89
+ for (auto k = 0; k < alpha_c; k++) {
90
+ total += exp(sum_j + logbeta_bd_i_j_k[i + j + k] - log_bc_k_logbeta_k_bc[k]);
91
+ }
92
+ }
93
+ }
94
+
95
+ return 1 - prob_b_beats_a(alpha_a, beta_a, alpha_d, beta_d) -
96
+ prob_b_beats_a(alpha_b, beta_b, alpha_d, beta_d) -
97
+ prob_b_beats_a(alpha_c, beta_c, alpha_d, beta_d) +
98
+ prob_c_beats_a_and_b(alpha_a, beta_a, alpha_b, beta_b, alpha_d, beta_d) +
99
+ prob_c_beats_a_and_b(alpha_a, beta_a, alpha_c, beta_c, alpha_d, beta_d) +
100
+ prob_c_beats_a_and_b(alpha_b, beta_b, alpha_c, beta_c, alpha_d, beta_d) - total;
101
+ }
102
+
103
+ }
@@ -0,0 +1,12 @@
1
+ #include <rice/rice.hpp>
2
+ #include "bayesian_ab.hpp"
3
+
4
+ extern "C"
5
+ void Init_ext() {
6
+ auto rb_mFieldTest = Rice::define_module("FieldTest");
7
+
8
+ Rice::define_class_under(rb_mFieldTest, "Calculations")
9
+ .define_singleton_function("prob_b_beats_a", &bayesian_ab::prob_b_beats_a)
10
+ .define_singleton_function("prob_c_beats_a_and_b", &bayesian_ab::prob_c_beats_a_and_b)
11
+ .define_singleton_function("prob_d_beats_a_and_b_and_c", &bayesian_ab::prob_d_beats_a_and_b_and_c);
12
+ }
@@ -0,0 +1,5 @@
1
+ require "mkmf-rice"
2
+
3
+ $CXXFLAGS << " -std=c++17 $(optflags)"
4
+
5
+ create_makefile("field_test/ext")
@@ -22,11 +22,16 @@ module FieldTest
22
22
  options[:user_agent] = request.user_agent
23
23
  end
24
24
 
25
- # cache results for request
26
- @field_test_cache ||= {}
27
-
28
25
  # don't update variant when passed via params
29
- @field_test_cache[experiment] ||= params_variant || exp.variant(participants, options)
26
+ if params_variant
27
+ params_variant
28
+ else
29
+ # cache results for request
30
+ # TODO possibly remove in 0.4.0
31
+ cache_key = [exp.id, participants.map(&:where_values), options.slice(:variant, :exclude)]
32
+ @field_test_cache ||= {}
33
+ @field_test_cache[cache_key] ||= exp.variant(participants, options)
34
+ end
30
35
  end
31
36
 
32
37
  def field_test_converted(experiment, **options)
@@ -1,3 +1,3 @@
1
1
  module FieldTest
2
- VERSION = "0.3.2"
2
+ VERSION = "0.5.1"
3
3
  end
data/lib/field_test.rb CHANGED
@@ -3,8 +3,10 @@ require "active_support"
3
3
  require "browser"
4
4
  require "ipaddr"
5
5
 
6
+ # ext
7
+ require "field_test/ext"
8
+
6
9
  # modules
7
- require "field_test/calculations"
8
10
  require "field_test/experiment"
9
11
  require "field_test/helpers"
10
12
  require "field_test/participant"
@@ -3,7 +3,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version
3
3
  create_table :field_test_events do |t|
4
4
  t.references :field_test_membership
5
5
  t.string :name
6
- t.timestamp :created_at
6
+ t.datetime :created_at
7
7
  end
8
8
  end
9
9
  end
@@ -5,7 +5,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version
5
5
  t.string :participant_id
6
6
  t.string :experiment
7
7
  t.string :variant
8
- t.timestamp :created_at
8
+ t.datetime :created_at
9
9
  t.boolean :converted, default: false
10
10
  end
11
11
 
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.2
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-04-16 00:00:00.000000000 Z
11
+ date: 2021-09-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: railties
@@ -16,42 +16,28 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '5'
19
+ version: '5.2'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '5'
26
+ version: '5.2'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: activerecord
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '5'
33
+ version: '5.2'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '5'
41
- - !ruby/object:Gem::Dependency
42
- name: distribution
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - ">="
46
- - !ruby/object:Gem::Version
47
- version: '0'
48
- type: :runtime
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - ">="
53
- - !ruby/object:Gem::Version
54
- version: '0'
40
+ version: '5.2'
55
41
  - !ruby/object:Gem::Dependency
56
42
  name: browser
57
43
  requirement: !ruby/object:Gem::Requirement
@@ -67,93 +53,24 @@ dependencies:
67
53
  - !ruby/object:Gem::Version
68
54
  version: '2.0'
69
55
  - !ruby/object:Gem::Dependency
70
- name: bundler
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - ">="
74
- - !ruby/object:Gem::Version
75
- version: '0'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - ">="
81
- - !ruby/object:Gem::Version
82
- version: '0'
83
- - !ruby/object:Gem::Dependency
84
- name: rake
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - ">="
88
- - !ruby/object:Gem::Version
89
- version: '0'
90
- type: :development
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - ">="
95
- - !ruby/object:Gem::Version
96
- version: '0'
97
- - !ruby/object:Gem::Dependency
98
- name: minitest
56
+ name: rice
99
57
  requirement: !ruby/object:Gem::Requirement
100
58
  requirements:
101
59
  - - ">="
102
60
  - !ruby/object:Gem::Version
103
- version: '0'
104
- type: :development
105
- prerelease: false
106
- version_requirements: !ruby/object:Gem::Requirement
107
- requirements:
108
- - - ">="
109
- - !ruby/object:Gem::Version
110
- version: '0'
111
- - !ruby/object:Gem::Dependency
112
- name: combustion
113
- requirement: !ruby/object:Gem::Requirement
114
- requirements:
115
- - - ">="
116
- - !ruby/object:Gem::Version
117
- version: '0'
118
- type: :development
119
- prerelease: false
120
- version_requirements: !ruby/object:Gem::Requirement
121
- requirements:
122
- - - ">="
123
- - !ruby/object:Gem::Version
124
- version: '0'
125
- - !ruby/object:Gem::Dependency
126
- name: rails
127
- requirement: !ruby/object:Gem::Requirement
128
- requirements:
129
- - - ">="
130
- - !ruby/object:Gem::Version
131
- version: '0'
132
- type: :development
133
- prerelease: false
134
- version_requirements: !ruby/object:Gem::Requirement
135
- requirements:
136
- - - ">="
137
- - !ruby/object:Gem::Version
138
- version: '0'
139
- - !ruby/object:Gem::Dependency
140
- name: sqlite3
141
- requirement: !ruby/object:Gem::Requirement
142
- requirements:
143
- - - ">="
144
- - !ruby/object:Gem::Version
145
- version: '0'
146
- type: :development
61
+ version: 4.0.2
62
+ type: :runtime
147
63
  prerelease: false
148
64
  version_requirements: !ruby/object:Gem::Requirement
149
65
  requirements:
150
66
  - - ">="
151
67
  - !ruby/object:Gem::Version
152
- version: '0'
153
- description:
154
- email: andrew@chartkick.com
68
+ version: 4.0.2
69
+ description:
70
+ email: andrew@ankane.org
155
71
  executables: []
156
- extensions: []
72
+ extensions:
73
+ - ext/field_test/extconf.rb
157
74
  extra_rdoc_files: []
158
75
  files:
159
76
  - CHANGELOG.md
@@ -172,8 +89,10 @@ files:
172
89
  - app/views/field_test/participants/show.html.erb
173
90
  - app/views/layouts/field_test/application.html.erb
174
91
  - config/routes.rb
92
+ - ext/field_test/bayesian_ab.hpp
93
+ - ext/field_test/ext.cpp
94
+ - ext/field_test/extconf.rb
175
95
  - lib/field_test.rb
176
- - lib/field_test/calculations.rb
177
96
  - lib/field_test/controller.rb
178
97
  - lib/field_test/engine.rb
179
98
  - lib/field_test/experiment.rb
@@ -190,7 +109,7 @@ homepage: https://github.com/ankane/field_test
190
109
  licenses:
191
110
  - MIT
192
111
  metadata: {}
193
- post_install_message:
112
+ post_install_message:
194
113
  rdoc_options: []
195
114
  require_paths:
196
115
  - lib
@@ -198,15 +117,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
198
117
  requirements:
199
118
  - - ">="
200
119
  - !ruby/object:Gem::Version
201
- version: '2.4'
120
+ version: '2.6'
202
121
  required_rubygems_version: !ruby/object:Gem::Requirement
203
122
  requirements:
204
123
  - - ">="
205
124
  - !ruby/object:Gem::Version
206
125
  version: '0'
207
126
  requirements: []
208
- rubygems_version: 3.1.2
209
- signing_key:
127
+ rubygems_version: 3.2.22
128
+ signing_key:
210
129
  specification_version: 4
211
130
  summary: A/B testing for Rails
212
131
  test_files: []
@@ -1,58 +0,0 @@
1
- require "distribution/math_extension"
2
-
3
- # formulas from
4
- # https://www.evanmiller.org/bayesian-ab-testing.html
5
- module FieldTest
6
- module Calculations
7
- def self.prob_b_beats_a(alpha_a, beta_a, alpha_b, beta_b)
8
- total = 0.0
9
-
10
- # for performance
11
- logbeta_aa_ba = Math.logbeta(alpha_a, beta_a)
12
- beta_ba = beta_b + beta_a
13
-
14
- 0.upto(alpha_b - 1) do |i|
15
- total += Math.exp(Math.logbeta(alpha_a + i, beta_ba) -
16
- Math.log(beta_b + i) - Math.logbeta(1 + i, beta_b) -
17
- logbeta_aa_ba)
18
- end
19
-
20
- total
21
- end
22
-
23
- def self.prob_c_beats_a_and_b(alpha_a, beta_a, alpha_b, beta_b, alpha_c, beta_c)
24
- total = 0.0
25
-
26
- # for performance
27
- logbeta_ac_bc = Math.logbeta(alpha_c, beta_c)
28
- abc = beta_a + beta_b + beta_c
29
- log_bb_j = []
30
- logbeta_j_bb = []
31
- logbeta_ac_i_j = []
32
- 0.upto(alpha_b - 1) do |j|
33
- log_bb_j[j] = Math.log(beta_b + j)
34
- logbeta_j_bb[j] = Math.logbeta(1 + j, beta_b)
35
-
36
- 0.upto(alpha_a - 1) do |i|
37
- logbeta_ac_i_j[i + j] ||= Math.logbeta(alpha_c + i + j, abc)
38
- end
39
- end
40
-
41
- 0.upto(alpha_a - 1) do |i|
42
- # for performance
43
- log_ba_i = Math.log(beta_a + i)
44
- logbeta_i_ba = Math.logbeta(1 + i, beta_a)
45
-
46
- 0.upto(alpha_b - 1) do |j|
47
- total += Math.exp(logbeta_ac_i_j[i + j] -
48
- log_ba_i - log_bb_j[j] -
49
- logbeta_i_ba - logbeta_j_bb[j] -
50
- logbeta_ac_bc)
51
- end
52
- end
53
-
54
- 1 - prob_b_beats_a(alpha_c, beta_c, alpha_a, beta_a) -
55
- prob_b_beats_a(alpha_c, beta_c, alpha_b, beta_b) + total
56
- end
57
- end
58
- end