field_test 0.2.4 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,86 +1,57 @@
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 = field_test_participants(options)
6
+ participants = FieldTest::Participant.standardize(options[:participant] || field_test_participant)
7
7
 
8
8
  if try(:request)
9
- if params[:field_test] && params[:field_test][experiment]
10
- options[:variant] ||= params[:field_test][experiment]
9
+ options = options.dup
10
+
11
+ if !options[:variant] && params[:field_test] && params[:field_test][experiment] && exp.variants.include?(params[:field_test][experiment])
12
+ params_variant = params[:field_test][experiment]
11
13
  end
12
14
 
13
15
  if FieldTest.exclude_bots?
14
16
  options[:exclude] = Browser.new(request.user_agent).bot?
15
17
  end
16
18
 
19
+ options[:exclude] ||= FieldTest.excluded_ips.any? { |ip| ip.include?(request.remote_ip) }
20
+
17
21
  options[:ip] = request.remote_ip
18
22
  options[:user_agent] = request.user_agent
19
23
  end
20
24
 
21
- # cache results for request
22
- @field_test_cache ||= {}
23
- @field_test_cache[experiment] ||= exp.variant(participants, options)
25
+ # don't update variant when passed via params
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
24
35
  end
25
36
 
26
- def field_test_converted(experiment, options = {})
37
+ def field_test_converted(experiment, **options)
27
38
  exp = FieldTest::Experiment.find(experiment)
28
39
 
29
- participants = field_test_participants(options)
40
+ participants = FieldTest::Participant.standardize(options[:participant] || field_test_participant)
30
41
 
31
42
  exp.convert(participants, goal: options[:goal])
32
43
  end
33
44
 
34
- def field_test_experiments(options = {})
35
- participants = field_test_participants(options)
36
- memberships = FieldTest::Membership.where(participant: participants).group_by(&:participant)
45
+ # TODO fetch in single query
46
+ def field_test_experiments(**options)
47
+ participants = FieldTest::Participant.standardize(options[:participant] || field_test_participant)
37
48
  experiments = {}
38
49
  participants.each do |participant|
39
- memberships[participant].to_a.each do |membership|
50
+ FieldTest::Membership.where(participant.where_values).each do |membership|
40
51
  experiments[membership.experiment] ||= membership.variant
41
52
  end
42
53
  end
43
54
  experiments
44
55
  end
45
-
46
- def field_test_participants(options = {})
47
- participants = []
48
-
49
- if options[:participant]
50
- participants << options[:participant]
51
- else
52
- if respond_to?(:current_user, true) && current_user
53
- participants << current_user
54
- end
55
-
56
- # controllers and views
57
- if try(:request)
58
- # use cookie
59
- cookie_key = "field_test"
60
-
61
- token = cookies[cookie_key]
62
- token = token.gsub(/[^a-z0-9\-]/i, "") if token
63
-
64
- if participants.empty? && !token
65
- token = SecureRandom.uuid
66
- cookies[cookie_key] = {value: token, expires: 30.days.from_now}
67
- end
68
- if token
69
- participants << token
70
-
71
- # backwards compatibility
72
- participants << "cookie:#{token}"
73
- end
74
- end
75
-
76
- # mailers
77
- to = try(:message).try(:to).try(:first)
78
- if to
79
- participants << to
80
- end
81
- end
82
-
83
- FieldTest::Participant.standardize(participants)
84
- end
85
56
  end
86
57
  end
@@ -0,0 +1,20 @@
1
+ module FieldTest
2
+ module Mailer
3
+ extend ActiveSupport::Concern
4
+ include Helpers
5
+
6
+ included do
7
+ helper_method :field_test
8
+ helper_method :field_test_converted
9
+ helper_method :field_test_experiments
10
+ end
11
+
12
+ def field_test_participant
13
+ if @user
14
+ @user
15
+ elsif respond_to?(:params) && params
16
+ params[:user]
17
+ end
18
+ end
19
+ end
20
+ end
@@ -1,7 +1,38 @@
1
1
  module FieldTest
2
2
  class Participant
3
- def self.standardize(participants)
4
- Array(participants).map { |v| v.respond_to?(:model_name) ? "#{v.model_name.name}:#{v.id}" : v.to_s }
3
+ attr_reader :type, :id
4
+
5
+ def initialize(object)
6
+ if object.is_a?(FieldTest::Participant)
7
+ @type = object.type
8
+ @id = object.id
9
+ elsif object.respond_to?(:model_name)
10
+ @type = object.model_name.name
11
+ @id = object.id.to_s
12
+ else
13
+ @id = object.to_s
14
+ end
15
+ end
16
+
17
+ def participant
18
+ [type, id].compact.join(":")
19
+ end
20
+
21
+ def where_values
22
+ if FieldTest.legacy_participants
23
+ {
24
+ participant: participant
25
+ }
26
+ else
27
+ {
28
+ participant_type: type,
29
+ participant_id: id
30
+ }
31
+ end
32
+ end
33
+
34
+ def self.standardize(participant)
35
+ Array(participant).compact.map { |v| FieldTest::Participant.new(v) }
5
36
  end
6
37
  end
7
38
  end
@@ -1,3 +1,3 @@
1
1
  module FieldTest
2
- VERSION = "0.2.4"
2
+ VERSION = "0.4.1"
3
3
  end
@@ -1,32 +1,17 @@
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 Rails::Generators::Migration
10
- source_root File.expand_path("../templates", __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
24
11
  end
25
12
 
26
13
  def migration_version
27
- if ActiveRecord::VERSION::MAJOR >= 5
28
- "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
29
- end
14
+ "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
30
15
  end
31
16
  end
32
17
  end
@@ -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 Rails::Generators::Migration
10
- source_root File.expand_path("../templates", __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
@@ -28,9 +15,7 @@ module FieldTest
28
15
  end
29
16
 
30
17
  def migration_version
31
- if ActiveRecord::VERSION::MAJOR >= 5
32
- "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
33
- end
18
+ "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
34
19
  end
35
20
  end
36
21
  end
@@ -1,11 +1,9 @@
1
1
  class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version %>
2
2
  def change
3
3
  create_table :field_test_events do |t|
4
- t.integer :field_test_membership_id
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
-
9
- add_index :field_test_events, :field_test_membership_id
10
8
  end
11
9
  end
@@ -1,15 +1,16 @@
1
1
  class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version %>
2
2
  def change
3
3
  create_table :field_test_memberships do |t|
4
- t.string :participant
4
+ t.string :participant_type
5
+ t.string :participant_id
5
6
  t.string :experiment
6
7
  t.string :variant
7
- t.timestamp :created_at
8
+ t.datetime :created_at
8
9
  t.boolean :converted, default: false
9
10
  end
10
11
 
11
- add_index :field_test_memberships, [:experiment, :participant], unique: true
12
- add_index :field_test_memberships, :participant
12
+ add_index :field_test_memberships, [:participant_type, :participant_id, :experiment],
13
+ unique: true, name: "index_field_test_memberships_on_participant"
13
14
  add_index :field_test_memberships, [:experiment, :created_at]
14
15
  end
15
16
  end
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.2.4
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
8
  autorequire:
9
- bindir: exe
9
+ bindir: bin
10
10
  cert_chain: []
11
- date: 2019-01-04 00:00:00.000000000 Z
11
+ date: 2020-09-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: railties
@@ -16,28 +16,28 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '0'
19
+ version: '5'
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: '0'
26
+ version: '5'
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: '0'
33
+ version: '5'
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: '0'
40
+ version: '5'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: distribution
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -56,14 +56,14 @@ dependencies:
56
56
  name: browser
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - "~>"
59
+ - - ">="
60
60
  - !ruby/object:Gem::Version
61
61
  version: '2.0'
62
62
  type: :runtime
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
- - - "~>"
66
+ - - ">="
67
67
  - !ruby/object:Gem::Version
68
68
  version: '2.0'
69
69
  - !ruby/object:Gem::Dependency
@@ -108,23 +108,62 @@ dependencies:
108
108
  - - ">="
109
109
  - !ruby/object:Gem::Version
110
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
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
111
153
  description:
112
- email:
113
- - andrew@chartkick.com
154
+ email: andrew@chartkick.com
114
155
  executables: []
115
156
  extensions: []
116
157
  extra_rdoc_files: []
117
158
  files:
118
- - ".gitignore"
119
159
  - CHANGELOG.md
120
- - Gemfile
121
160
  - LICENSE.txt
122
161
  - README.md
123
- - Rakefile
124
162
  - app/controllers/field_test/base_controller.rb
125
163
  - app/controllers/field_test/experiments_controller.rb
126
164
  - app/controllers/field_test/memberships_controller.rb
127
165
  - app/controllers/field_test/participants_controller.rb
166
+ - app/helpers/field_test/base_helper.rb
128
167
  - app/models/field_test/event.rb
129
168
  - app/models/field_test/membership.rb
130
169
  - app/views/field_test/experiments/_experiments.html.erb
@@ -133,12 +172,13 @@ files:
133
172
  - app/views/field_test/participants/show.html.erb
134
173
  - app/views/layouts/field_test/application.html.erb
135
174
  - config/routes.rb
136
- - field_test.gemspec
137
175
  - lib/field_test.rb
138
176
  - lib/field_test/calculations.rb
177
+ - lib/field_test/controller.rb
139
178
  - lib/field_test/engine.rb
140
179
  - lib/field_test/experiment.rb
141
180
  - lib/field_test/helpers.rb
181
+ - lib/field_test/mailer.rb
142
182
  - lib/field_test/participant.rb
143
183
  - lib/field_test/version.rb
144
184
  - lib/generators/field_test/events_generator.rb
@@ -147,7 +187,8 @@ files:
147
187
  - lib/generators/field_test/templates/events.rb.tt
148
188
  - lib/generators/field_test/templates/memberships.rb.tt
149
189
  homepage: https://github.com/ankane/field_test
150
- licenses: []
190
+ licenses:
191
+ - MIT
151
192
  metadata: {}
152
193
  post_install_message:
153
194
  rdoc_options: []
@@ -157,15 +198,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
157
198
  requirements:
158
199
  - - ">="
159
200
  - !ruby/object:Gem::Version
160
- version: '0'
201
+ version: '2.4'
161
202
  required_rubygems_version: !ruby/object:Gem::Requirement
162
203
  requirements:
163
204
  - - ">="
164
205
  - !ruby/object:Gem::Version
165
206
  version: '0'
166
207
  requirements: []
167
- rubyforge_project:
168
- rubygems_version: 2.7.6
208
+ rubygems_version: 3.1.2
169
209
  signing_key:
170
210
  specification_version: 4
171
211
  summary: A/B testing for Rails